(function ($) {

  Drupal.behaviors.leaflet = {
    attach:function (context, settings) {

      $(settings.leaflet).each(function () {
        // skip to the next iteration if the map already exists
        var container = L.DomUtil.get(this.mapId);
        if (!container || container._leaflet_id) {
          return;
        }

        // load a settings object with all of our map settings
        var settings = {};
        for (var setting in this.map.settings) {
          settings[setting] = this.map.settings[setting];
        }

        // instantiate our new map
        var lMap = new L.Map(this.mapId, settings);
        lMap.bounds = [];

        // add map layers
        var layers = {}, overlays = {};
        var i = 0;
        for (var key in this.map.layers) {
          var layer = this.map.layers[key];
          var map_layer = Drupal.leaflet.create_layer(layer, key);
          //layers[key] = map_layer;

          // Distinguish between "base layers" and "overlays".
          // Fall back to "base" in case "layer_type" has not been defined in
          // hook_leaflet_map_info()
          layer.layer_type = (typeof layer.layer_type === 'undefined') ? 'base' : layer.layer_type;
          // As stated in http://leafletjs.com/examples/layers-control,
          // when using multiple base layers, only one of them should be added
          // to the map at instantiation, but all of them should be present in
          // the base layers object when creating the layers control.
          // See statement L.control.layers(layers, overlays) much further below.
          switch (layer.layer_type) {
            case 'overlay':
              //lMap.addLayer(map_layer);
              overlays[key] = map_layer;
              break;
            default:
              if (i === 0 /*|| !this.map.settings.layerControl*/) {
                lMap.addLayer(map_layer);
                i++;
              }
              layers[key] = map_layer;
              break;
          }
          i++;
        }

        var switchEnable = false;
        for (var key in layers) {
          if (layers[key].options.switchLayer) {
            layers[key].setSwitchLayer(layers[layers[key].options.switchLayer]);
            switchEnable = true;
          }
        }
        if (switchEnable) {
          switchManager = new SwitchLayerManager(lMap, {baseLayers: layers});
        }

        // keep an instance of leaflet layers
        this.map.lLayers = layers;

        // keep an instance of map_id
        this.map.map_id = this.mapId;

        // add features
        for (i = 0; i < this.features.length; i++) {
          var feature = this.features[i];
          var lFeature;

          // dealing with a layer group
          if (feature.group) {
            var lGroup = new L.LayerGroup();
            for (var groupKey in feature.features) {
              var groupFeature = feature.features[groupKey];
              lFeature = leaflet_create_feature(groupFeature, lMap);
              if (groupFeature.popup) {
                lFeature.bindPopup(groupFeature.popup);
              }
              lGroup.addLayer(lFeature);

              // Allow others to do something with the feature within a group.
              $(document).trigger('leaflet.feature', [lFeature, feature]);
            }

            // add the group to the layer switcher
            overlays[feature.label] = lGroup;

            lMap.addLayer(lGroup);
          }
          else {
            lFeature = leaflet_create_feature(feature, lMap);
            lMap.addLayer(lFeature);

            if (feature.popup) {
              lFeature.bindPopup(feature.popup);
            }

            // Allow others to do something with the feature.
            $(document).trigger('leaflet.feature', [lFeature, feature]);
          }

        }

        // add layer switcher
        if (this.map.settings.layerControl) {
          L.control.layers(layers, overlays).addTo(lMap);
        }

        // center the map
        var zoom = this.map.settings.zoom ? this.map.settings.zoom : this.map.settings.zoomDefault;
        if (this.map.center && (this.map.center.force || this.features.length === 0)) {
          lMap.setView(new L.LatLng(this.map.center.lat, this.map.center.lon), zoom);
        }
        else if (this.features.length > 0) {
          Drupal.leaflet.fitbounds(lMap);
          if (this.map.settings.zoom) { // or: if (zoom) ?
            lMap.setZoom(zoom);
          }
        }

        // add attribution
        if (this.map.settings.attributionControl && this.map.attribution) {
          lMap.attributionControl.setPrefix(this.map.attribution.prefix);
          lMap.attributionControl.addAttribution(this.map.attribution.text);
        }

        // add the leaflet map to our settings object to make it accessible
        this.lMap = lMap;

        // allow other modules to get access to the map object using jQuery's trigger method
        $(document).trigger('leaflet.map', [this.map, lMap]);

        // Destroy features so that an AJAX reload does not get parts of the old set.
        // Required when the View has "Use AJAX" set to Yes.
        this.features = null;
      });

      function leaflet_create_feature(feature, lMap) {
        var lFeature;
        switch (feature.type) {
          case 'point':
            lFeature = Drupal.leaflet.create_point(feature, lMap);
            break;
          case 'linestring':
            lFeature = Drupal.leaflet.create_linestring(feature, lMap);
            break;
          case 'polygon':
            lFeature = Drupal.leaflet.create_polygon(feature, lMap);
            break;
          case 'multipolyline':
            feature.multipolyline = true;
            // no break;
          case 'multipolygon':
            lFeature = Drupal.leaflet.create_multipoly(feature, lMap);
            break;
          case 'json':
            lFeature = Drupal.leaflet.create_json(feature.json, lMap);
            break;
          case 'popup':
            lFeature = Drupal.leaflet.create_popup(feature, lMap);
            break;
          case 'circle':
            lFeature = Drupal.leaflet.create_circle(feature, lMap);
            break;
          case 'circlemarker':
            lFeature = Drupal.leaflet.create_circlemarker(feature, lMap);
            break;
          case 'rectangle':
            lFeature = Drupal.leaflet.create_rectangle(feature, lMap);
            break;
        }

        // assign our given unique ID, useful for associating nodes
        if (feature.leaflet_id) {
          lFeature._leaflet_id = feature.leaflet_id;
        }

        var options = {};
        if (feature.options) {
          for (var option in feature.options) {
            options[option] = feature.options[option];
          }
          lFeature.setStyle(options);
        }

        return lFeature;
      }

    }
  };

  Drupal.leaflet = {

    isOldVersion: function () {
      return !(parseInt(L.version) >= 1); // version may start with '0' or '.'
    },

    create_layer: function (layer, key) {
      // Use a Zoomswitch Layer extension to enable zoom-switch option.
      var map_layer = new L.TileLayerZoomSwitch(layer.urlTemplate);
      map_layer._leaflet_id = key;

      if (layer.options) {
        for (var option in layer.options) {
          map_layer.options[option] = layer.options[option];
        }
      }

      // layers served from TileStream need this correction in the y coordinates
      // TODO: Need to explore this more and find a more elegant solution
      if (layer.type == 'tilestream') {
        map_layer.getTileUrl = function (tilePoint) {
          this._adjustTilePoint(tilePoint);
          var zoom = this._getZoomForUrl();
          return L.Util.template(this._url, L.Util.extend({
            s: this._getSubdomain(tilePoint),
            z: zoom,
            x: tilePoint.x,
            y: Math.pow(2, zoom) - tilePoint.y - 1
          }, this.options));
        };
      }
      return map_layer;
    },

    create_circle: function(circle, lMap) {
      var latLng = new L.LatLng(circle.lat, circle.lon);
      latLng = latLng.wrap();
      lMap.bounds.push(latLng);
      if (circle.radius) {
        // @deprecated
        return L.circle(latLng, circle.radius, circle.options);
      }
      return new L.Circle(latLng, circle.options);
    },

    create_circlemarker: function(circle, lMap) {
      var latLng = new L.LatLng(circle.lat, circle.lon);
      latLng = latLng.wrap();
      lMap.bounds.push(latLng);
      return new L.CircleMarker(latLng, circle.options);
    },

    create_rectangle: function(box, lMap) {
      var bounds = box.bounds,
        southWest = new L.LatLng(bounds.s, bounds.w),
        northEast = new L.LatLng(bounds.n, bounds.e),
        latLng = new L.LatLngBounds(southWest, northEast);
      lMap.bounds.push(latLng);
      return new L.Rectangle(latLng, box.settings);
    },

    create_point: function(marker, lMap) {
      var latLng = new L.LatLng(marker.lat, marker.lon);
      latLng = latLng.wrap();
      lMap.bounds.push(latLng);
      var lMarker;

      if (marker.html) {
        if (marker.html_class) {
          var icon = new L.DivIcon({html: marker.html, className: marker.html_class});
        }
        else {
          var icon = new L.DivIcon({html: marker.html});
        }
        // override applicable marker defaults
        if (marker.icon.iconSize) {
          icon.options.iconSize = new L.Point(parseInt(marker.icon.iconSize.x, 10), parseInt(marker.icon.iconSize.y, 10));
        }
        if (marker.icon.iconAnchor) {
          icon.options.iconAnchor = new L.Point(parseFloat(marker.icon.iconAnchor.x), parseFloat(marker.icon.iconAnchor.y));
        }
        lMarker = new L.Marker(latLng, {icon:icon});
      }
      else if (marker.icon) {
        var icon = new L.Icon({iconUrl: marker.icon.iconUrl});

        // override applicable marker defaults
        if (marker.icon.iconSize) {
          icon.options.iconSize = new L.Point(parseInt(marker.icon.iconSize.x, 10), parseInt(marker.icon.iconSize.y, 10));
        }
        if (marker.icon.iconAnchor) {
          icon.options.iconAnchor = new L.Point(parseFloat(marker.icon.iconAnchor.x), parseFloat(marker.icon.iconAnchor.y));
        }
        if (marker.icon.popupAnchor) {
          icon.options.popupAnchor = new L.Point(parseFloat(marker.icon.popupAnchor.x), parseFloat(marker.icon.popupAnchor.y));
        }
        if (marker.icon.shadowUrl !== undefined) {
          icon.options.shadowUrl = marker.icon.shadowUrl;
        }
        if (marker.icon.shadowSize) {
          icon.options.shadowSize = new L.Point(parseInt(marker.icon.shadowSize.x, 10), parseInt(marker.icon.shadowSize.y, 10));
        }
        if (marker.icon.shadowAnchor) {
          icon.options.shadowAnchor = new L.Point(parseInt(marker.icon.shadowAnchor.x, 10), parseInt(marker.icon.shadowAnchor.y, 10));
        }
        if (marker.icon.zIndexOffset) {
          icon.options.zIndexOffset = marker.icon.zIndexOffset;
        }
        if (marker.icon.className) {
          icon.options.className = marker.icon.className;
        }
        var options = {icon:icon};
        if (marker.zIndexOffset) {
          options.zIndexOffset = marker.zIndexOffset;
        }
        lMarker = new L.Marker(latLng, options);
      }
      else {
        lMarker = new L.Marker(latLng);
      }

      if (marker.label) {
        lMarker.options.title = marker.label;
      }

      return lMarker;
    },

    create_linestring: function(polyline, lMap) {
      var latlngs = [];
      for (var i = 0; i < polyline.points.length; i++) {
        var latlng = new L.LatLng(polyline.points[i].lat, polyline.points[i].lon);
        latlng = latlng.wrap();
        latlngs.push(latlng);
        lMap.bounds.push(latlng);
      }
      return new L.Polyline(latlngs);
    },

    create_polygon: function(polygon, lMap) {
      var latlngs = [];
      for (var i = 0; i < polygon.points.length; i++) {
        var latlng = new L.LatLng(polygon.points[i].lat, polygon.points[i].lon);
        latlng = latlng.wrap();
        latlngs.push(latlng);
        lMap.bounds.push(latlng);
      }
      return new L.Polygon(latlngs);
    },

    create_multipoly: function(multipoly, lMap) {
      var polygons = [];
      for (var x = 0; x < multipoly.component.length; x++) {
        var latlngs = [];
        var polygon = multipoly.component[x];
        for (var i = 0; i < polygon.points.length; i++) {
          var latlng = new L.LatLng(polygon.points[i].lat, polygon.points[i].lon);
          latlng = latlng.wrap();
          latlngs.push(latlng);
          lMap.bounds.push(latlng);
        }
        polygons.push(latlngs);
      }
      if (this.isOldVersion()) {
        return multipoly.multipolyline ? new L.MultiPolyline(polygons) : new L.MultiPolygon(polygons);
      }
      return multipoly.multipolyline ? new L.Polyline(polygons): new L.Polygon(polygons);
    },

    create_json:function(json, lMap) {
      lJSON = new L.GeoJSON(json, {
        onEachFeature:function (feature, layer) {
          var has_properties = (typeof feature.properties != 'undefined');

          // bind popups
          if (has_properties && typeof feature.properties.popup != 'undefined') {
            layer.bindPopup(feature.properties.popup);
          }

          for (var layer_id in layer._layers) {
            for (var i in layer._layers[layer_id]._latlngs) {
              lMap.bounds.push(layer._layers[layer_id]._latlngs[i]);
            }
          }

          if (has_properties && typeof feature.properties.style != 'undefined') {
            layer.setStyle(feature.properties.style);
          }

          if (has_properties && typeof feature.properties.leaflet_id != 'undefined') {
            layer._leaflet_id = feature.properties.leaflet_id;
          }
        }
      });

      return lJSON;
    },

    create_popup: function(popup) {
      var latLng = new L.LatLng(popup.lat, popup.lon);
      this.bounds.push(latLng);
      var lPopup = new L.Popup();
      lPopup.setLatLng(latLng);
      if (popup.content) {
        lPopup.setContent(popup.content);
      }
      return lPopup;
    },

    fitbounds:function (lMap) {
      if (lMap.bounds.length > 0) {
        lMap.fitBounds(new L.LatLngBounds(lMap.bounds));
      }
    }
  };

  // Zoomswitch method cribbed liberally from:
  // http://www.makina-corpus.org/blog/leaflet-zoom-switcher
  L.TileLayerZoomSwitch = L.TileLayer.extend({
    includes: L.Mixin.Events,

    options: {
      // switchZoomUnder: when zoom < switchZoomUnder, then switch to switchLayer
      switchZoomUnder: -1,
      // switchZoomAbove: when zoom >= switchZoomAbove, then switch to switchLayer
      switchZoomAbove: -1,
      switchLayer: null
    },

    setSwitchLayer: function (layer) {
      this.options.switchLayer = layer;
    },

    getSwitchZoomUnder: function () {
      return this.options.switchZoomUnder;
    },

    getSwitchZoomAbove: function () {
      return this.options.switchZoomAbove;
    },

    getSwitchLayer: function () {
      return this.options.switchLayer;
    }

  });

  L.tileLayerZoomSwitch = function (url, options) {
    return new L.TileLayerZoomSwitch(url, options);
  };

  /*
   * SwitchLayerManager is a custom class for managing base layer automatic switching according to the current zoom level
   */

  SwitchLayerManager = L.Class.extend({

    _map: null,

    options: {
      baseLayers: null
    },

    initialize: function (map, options) {
      this._map = map;
      L.Util.setOptions(this, options);

      this._map.on({
        'zoomend': this._update
      }, this)

    },

    _update: function (e) {
      var zoom = this._map.getZoom();

      for (var i in this.options.baseLayers) {
        var curBL = this.options.baseLayers[i];
        var zoomUnder = curBL.getSwitchZoomUnder();
        var zoomAbove = curBL.getSwitchZoomAbove();
        var switchLayer = curBL.getSwitchLayer();

        // If layer got a switchlayer, and if layer actually displayed
        if (switchLayer && curBL._map != null) {
        //if (switchLayer) {
          if(zoomUnder != -1 && zoom < zoomUnder) {
            this._map.removeLayer(curBL);
            this._map.addLayer(switchLayer, false);
          }

          if(zoomAbove != -1 && zoom >= zoomAbove) {
            this._map.removeLayer(curBL);
            this._map.addLayer(switchLayer, false);
          }
        }
      }
    }
  });

})(jQuery);
