(function () {

L.Handler.MarkerSnap = L.Handler.extend({
    options: {
        snapDistance: 15, // in pixels
        snapVertices: true
    },

    initialize: function (map, marker, options) {
        L.Handler.prototype.initialize.call(this, map);
        this._markers = [];
        this._guides = [];

        if (arguments.length == 2) {
            if (!(marker instanceof L.Class)) {
                options = marker;
                marker = null;
            }
        }

        L.Util.setOptions(this, options || {});

        if (marker) {
            // new markers should be draggable !
            if (!marker.dragging) marker.dragging = new L.Handler.MarkerDrag(marker);
            marker.dragging.enable();
            this.watchMarker(marker);
        }

        // Convert snap distance in pixels into buffer in degres, for searching around mouse
        // It changes at each zoom change.
        function computeBuffer() {
            this._buffer = map.layerPointToLatLng(new L.Point(0,0)).lat -
                           map.layerPointToLatLng(new L.Point(this.options.snapDistance, 0)).lat;
        }
        map.on('zoomend', computeBuffer, this);
        map.whenReady(computeBuffer, this);
        computeBuffer.call(this);
    },

    enable: function () {
        this.disable();
        for (var i=0; i<this._markers.length; i++) {
            this.watchMarker(this._markers[i]);
        }
    },

    disable: function () {
        for (var i=0; i<this._markers.length; i++) {
            this.unwatchMarker(this._markers[i]);
        }
    },

    watchMarker: function (marker) {
        if (this._markers.indexOf(marker) == -1)
            this._markers.push(marker);
        marker.on('move', this._snapMarker, this);
    },

    unwatchMarker: function (marker) {
        marker.off('move', this._snapMarker, this);
        delete marker['snap'];
    },

    addGuideLayer: function (layer) {
        for (var i=0, n=this._guides.length; i<n; i++)
            if (L.stamp(layer) === L.stamp(this._guides[i]))
                return;
        this._guides.push(layer);
    },

    _snapMarker: function(e) {
        var marker = e.target,
            latlng = marker.getLatLng(),
            snaplist = [];

        function isDifferentLayer(layer) {
            if (layer.getLatLng) {
                return L.stamp(marker) !== L.stamp(layer);
            } else {
                if (layer.editing && layer.editing._enabled) {
                    var points = layer.editing._verticesHandlers[0]._markerGroup.getLayers();
                    for(var i = 0, n = points.length; i < n; i++) {
                        if (L.stamp(points[i]) === L.stamp(marker)) { return false; }
                    }
                }
            }

            return true;
        }

        function processGuide(guide) {
            if ((guide._layers !== undefined) &&
                (typeof guide.searchBuffer !== 'function')) {
                // Guide is a layer group and has no L.LayerIndexMixin (from Leaflet.LayerIndex)
                for (var id in guide._layers) {
                    processGuide(guide._layers[id]);
                }
            }
            else if (typeof guide.searchBuffer === 'function') {
                // Search snaplist around mouse
                var nearlayers = guide.searchBuffer(latlng, this._buffer);
                snaplist = snaplist.concat(nearlayers.filter(function(layer) {
                    return isDifferentLayer(layer);
                }));
            }
            // Make sure the marker doesn't snap to itself or the associated polyline layer
            else if (isDifferentLayer(guide)) {
                snaplist.push(guide);
            }
        }

        for (var i=0, n = this._guides.length; i < n; i++) {
            var guide = this._guides[i];
            processGuide.call(this, guide);
        }

        var closest = this._findClosestLayerSnap(this._map,
                                                 snaplist,
                                                 latlng,
                                                 this.options.snapDistance,
                                                 this.options.snapVertices);

        closest = closest || {layer: null, latlng: null};
        this._updateSnap(marker, closest.layer, closest.latlng);
    },

    _findClosestLayerSnap: function (map, layers, latlng, tolerance, withVertices) {
        return L.GeometryUtil.closestLayerSnap(map, layers, latlng, tolerance, withVertices);
    },

    _updateSnap: function (marker, layer, latlng) {
        if (layer && latlng) {
            marker._latlng = L.latLng(latlng);
            marker.update();
            if (marker.snap != layer) {
                marker.snap = layer;
                if (marker._icon) L.DomUtil.addClass(marker._icon, 'marker-snapped');
                marker.fire('snap', {layer:layer, latlng: latlng});
            }
        }
        else {
            if (marker.snap) {
                if (marker._icon) L.DomUtil.removeClass(marker._icon, 'marker-snapped');
                marker.fire('unsnap', {layer:marker.snap});
            }
            delete marker['snap'];
        }
    }
});


if (!L.Edit) {
    // Leaflet.Draw not available.
    return;
}


L.Handler.PolylineSnap = L.Edit.Poly.extend({

    initialize: function (map, poly, options) {
        var that = this;

        L.Edit.Poly.prototype.initialize.call(this, poly, options);
        this._snapper = new L.Handler.MarkerSnap(map, options);
        poly.on('remove', function() {
            that.disable();
        });
    },

    addGuideLayer: function (layer) {
        this._snapper.addGuideLayer(layer);
    },
    
    _initHandlers: function () {
        this._verticesHandlers = [];
        for (var i = 0; i < this.latlngs.length; i++) {
            this._verticesHandlers.push(new L.Edit.PolyVerticesEditSnap(this._poly, this.latlngs[i], this.options));
        }
    }
});

L.Edit.PolyVerticesEditSnap = L.Edit.PolyVerticesEdit.extend({
    _createMarker: function (latlng, index) {
               var marker = L.Edit.PolyVerticesEdit.prototype._createMarker.call(this, latlng, index);

        // Treat middle markers differently
        var isMiddle = index === undefined;
        if (isMiddle) {
            // Snap middle markers, only once they were touched
            marker.on('dragstart', function () {
                this._poly.snapediting._snapper.watchMarker(marker);
            }, this);
        }
        else {
            this._poly.snapediting._snapper.watchMarker(marker);
        }
        return marker;
    }
});

L.EditToolbar.SnapEdit = L.EditToolbar.Edit.extend({
    snapOptions: {
        snapDistance: 15, // in pixels
        snapVertices: true
    },

    initialize: function(map, options) {
        L.EditToolbar.Edit.prototype.initialize.call(this, map, options);

        if (options.snapOptions) {
            L.Util.extend(this.snapOptions, options.snapOptions);
        }

        if (Array.isArray(this.snapOptions.guideLayers)) {
            this._guideLayers = this.snapOptions.guideLayers;
        } else if (options.guideLayers instanceof L.LayerGroup) {
            this._guideLayers = this.snapOptions.guideLayers.getLayers();
        } else {
            this._guideLayers = [];
        }
    },

    addGuideLayer: function(layer) {
        var index = this._guideLayers.findIndex(function(guideLayer) {
            return L.stamp(layer) === L.stamp(guideLayer);
        });

        if (index === -1) {
            this._guideLayers.push(layer);
            this._featureGroup.eachLayer(function(layer) {
                if (layer.snapediting) { layer.snapediting._guides.push(layer); }
            });
        }
    },

    removeGuideLayer: function(layer) {
      var index = this._guideLayers.findIndex(function(guideLayer) {
          return L.stamp(layer) === L.stamp(guideLayer);
      });

      if (index !== -1) {
          this._guideLayers.splice(index, 1);
          this._featureGroup.eachLayer(function(layer) {
              if (layer.snapediting) { layer.snapediting._guides.splice(index, 1); }
          });
      }
    },

    clearGuideLayers: function() {
        this._guideLayers = [];
        this._featureGroup.eachLayer(function(layer) {
            if (layer.snapediting) { layer.snapediting._guides = []; }
        });
    },

    _enableLayerEdit: function(e) {
        L.EditToolbar.Edit.prototype._enableLayerEdit.call(this, e);

        var layer = e.layer || e.target || e;

        if (!layer.snapediting) {
            if (layer.getLatLng) {
                layer.snapediting = new L.Handler.MarkerSnap(layer._map, layer, this.snapOptions);
            } else {
                if (layer.editing) {
                  layer.editing._verticesHandlers[0]._markerGroup.clearLayers();
                  delete layer.editing;
                }

                layer.editing = layer.snapediting = new L.Handler.PolylineSnap(layer._map, layer, this.snapOptions);
            }

            for (var i = 0, n = this._guideLayers.length; i < n; i++) {
                layer.snapediting.addGuideLayer(this._guideLayers[i]);
            }
        }

        layer.snapediting.enable();
    }
});

L.Draw.Feature.SnapMixin = {
    _snap_initialize: function () {
        this.on('enabled', this._snap_on_enabled, this);
        this.on('disabled', this._snap_on_disabled, this);
    },

    _snap_on_enabled: function () {
        if (!this.options.guideLayers) {
            return;
        }

        if (!this._mouseMarker) {
            this._map.on('layeradd', this._snap_on_enabled, this);
            return;
        }else{
            this._map.off('layeradd', this._snap_on_enabled, this);
        }

        if (!this._snapper) {
            this._snapper = new L.Handler.MarkerSnap(this._map);
            if (this.options.snapDistance) {
                this._snapper.options.snapDistance = this.options.snapDistance;
            }
            if (this.options.snapVertices) {
                this._snapper.options.snapVertices = this.options.snapVertices;
            }
        }

        for (var i=0, n=this.options.guideLayers.length; i<n; i++)
            this._snapper.addGuideLayer(this.options.guideLayers[i]);

        var marker = this._mouseMarker;

        this._snapper.watchMarker(marker);

        // Show marker when (snap for user feedback)
        var icon = marker.options.icon;
        marker.on('snap', function (e) {
                  marker.setIcon(this.options.icon);
                  marker.setOpacity(1);
              }, this)
              .on('unsnap', function (e) {
                  marker.setIcon(icon);
                  marker.setOpacity(0);
              }, this);

        marker.on('click', this._snap_on_click, this);
    },

    _snap_on_click: function (e) {
        if (this._markers) {
            var markerCount = this._markers.length,
                marker = this._markers[markerCount - 1];
            if (this._mouseMarker.snap) {
                if(e){
                  // update the feature being drawn to reflect the snapped location:
                  marker.setLatLng(e.target._latlng);
                  if(this._poly){
                    var polyPointsCount = this._poly._latlngs.length;
                    this._poly._latlngs[polyPointsCount - 1] = e.target._latlng;
                    this._poly.redraw();
                  }
                }

                L.DomUtil.addClass(marker._icon, 'marker-snapped');
            }
        }
    },

    _snap_on_disabled: function () {
        delete this._snapper;
    },
};

L.Draw.Feature.include(L.Draw.Feature.SnapMixin);
L.Draw.Feature.addInitHook('_snap_initialize');

})();