(function($) {

    var mapDefaults = {
        zoom: 5,
        center: new google.maps.LatLng(40.273963, -76.884855),
        mapTypeControlOptions: { style: google.maps.MapTypeControlStyle.HORIZONTAL_BAR },
        mapTypeId: google.maps.MapTypeId.ROADMAP,
        pin: new google.maps.MarkerImage("/assets/images/map/pin.png", new google.maps.Size(17, 28)),
        shadow: new google.maps.MarkerImage("/assets/images/map/pin_shadow.png", new google.maps.Size(28, 36), null, new google.maps.Point(6, 31))
    };

    $.fn.extend({

        directionsMap: function(point, options) {
            if (this.length == 0) {
                return this;
            }

            options = $.extend({}, mapDefaults, { addressSelected: function() { } }, options || {});

            var map = new google.maps.Map(this[0], options),
				markers = [];

            // Draw the bubble only after the map has been loaded.
            var boundsChangedHandler = google.maps.event.addListener(map, "bounds_changed", drawBubble);


            return this;

            function drawBubble() {

                // The map is loaded so remove this listener.
                google.maps.event.removeListener(boundsChangedHandler);

                if (point && point.lat && point.lng) {
                    markers = [plotPoint(new google.maps.LatLng(point.lat, point.lng), point.title, point.address, point.link)];
                    map.setCenter(markers[0].position);
                    map.setZoom(15);

                    google.maps.event.trigger(markers[0], "click");
                }
            }

            function plotPoint(location, title, formattedAddress, link) {
                return plotPointOnMap(map, options, location, title, formattedAddress, link);
            }
        },

        // Search for addresses.
        geocodeMap: function(point, options) {
            if (this.length == 0) {
                return this;
            }

            options = $.extend({}, mapDefaults, options || {});

            var map = new google.maps.Map(this[0], options),
				geocoder = new google.maps.Geocoder(),
				markers = [];

            if (point && point.address && point.lat && point.lng) {
                markers = [plotPoint(new google.maps.LatLng(point.lat, point.lng), point.address)];
                resizeMapToFitPoints();
            }

            // Return a map object that has the findAddress method.
            return $.extend({ findAddress: findAddress }, this);

            function plotPoint(location, formattedAddress) {
                return plotPointOnMap(map, options, location, null, formattedAddress, null, clearBubbles);
            }

            function findAddress(search) {
                clearBubbles(true);

                geocoder.geocode({ address: search }, function(results, status) {
                    if (status != google.maps.GeocoderStatus.ZERO_RESULTS) {
                        var specificPoints = getMostSpecificPoints(results);

                        markers = $.map(specificPoints, function(result) {
                            return plotPoint(result.geometry.location, result.formatted_address);
                        });

                        if (markers.length == 1) {
                            google.maps.event.trigger(markers[0], "click");
                        }

                        resizeMapToFitPoints();
                    } else {
                        // No results
                    }
                });

            }

            function clearBubbles(clearMarker) {
                $.each(markers, function(i, marker) {
                    if (marker.bubble) {
                        marker.bubble.close();
                    }

                    if (clearMarker) {
                        marker.setMap(null);
                    }
                });
            }

            function resizeMapToFitPoints() {
                var bounds = new google.maps.LatLngBounds();

                $.each(markers, function(i, marker) {
                    // Visible is undefined the first right after the point is added to the map.
                    var visible = marker.getVisible();
                    if (visible === true || visible === undefined) {
                        bounds.extend(marker.position);
                    }
                });

                map.fitBounds(bounds);

                // Pull the map zoom level out a bit if it's in too far.
                if (map.getZoom() > 13) {
                    map.setZoom(13);
                }
            }

            function getMostSpecificPoints(results) {
                var locationTypes = [
            google.maps.GeocoderLocationType.ROOFTOP,
            google.maps.GeocoderLocationType.RANGE_INTERPOLATED,
            google.maps.GeocoderLocationType.GEOMETRIC_CENTER,
            google.maps.GeocoderLocationType.APPROXIMATE
          ];

                var filtered;
                $.each(locationTypes, function(i, type) {
                    filtered = $.grep(results, function(result) { return result.geometry.location_type == type; });
                    if (filtered.length > 0) {
                        return false;
                    }
                });

                return filtered;
            }
        },

        // Display event markers on a map.
        troegsMap: function(points, options) {

            if (this.length == 0) {
                return this;
            }

            points = points || [];

            options = $.extend({}, mapDefaults, options || {});


            var map = new google.maps.Map(this[0], options),
				markers = $.map(points, function(point) {

				    var marker = new google.maps.Marker({
				        position: new google.maps.LatLng(point.Latitude, point.Longitude),
				        map: map,
				        title: point.Title,
				        icon: options.pin,
				        shadow: options.shadow,
				        clickable: true
				    });

				    google.maps.event.addListener(marker, "click", function() {
				        clearBubbles();
				        marker.bubble = new TroegsBubble(map, this, $("#event_" + point.ID + "_details"));
				    });

				    google.maps.event.addListener(marker, "dblclick", function() {
				        this.map.setCenter(marker.position);
				        this.map.setZoom(this.map.getZoom() + 1);
				    });

				    return { point: point, marker: marker };
				});

            resizeMapToFitPoints();

            // Return a map object that has the filterPoints method.
            return $.extend({ filterPoints: filterPoints }, this);

            function resizeMapToFitPoints() {
                var bounds = new google.maps.LatLngBounds();

                $.each(markers, function(i, marker) {
                    // Visible is undefined the first right after the point is added to the map.
                    var visible = marker.marker.getVisible();
                    if (visible === true || visible === undefined) {
                        bounds.extend(marker.marker.position);
                    }
                });

                map.fitBounds(bounds);
                
                // Pull the map zoom level out a bit if it's in too far.
                if (map.getZoom() > 11) {
                    map.setZoom(11);
                }
            }

            function filterPoints(fnct) {
                clearBubbles();

                $.each(markers, function(i, marker) {
                    marker.marker.setVisible(fnct(marker.point));
                });

                resizeMapToFitPoints();
            }

            function clearBubbles() {
                $.each(markers, function(i, marker) {
                    if (marker.marker.bubble) {
                        marker.marker.bubble.close();
                    }
                });
            }
        }
    });

    // Used by directionsMap and geocodeMap.
    function plotPointOnMap(map, options, location, title, formattedAddress, link, click) {
        var marker = new google.maps.Marker({
            position: location,
            map: map,
            title: "Found!",
            icon: options.pin,
            shadow: options.shadow,
            clickable: true
        });

        google.maps.event.addListener(marker, "click", function() {
            if (click) {
                click();
            }

            formattedAddress = formattedAddress.replace(", USA", "");

            options.addressSelected(formattedAddress, location.lat(), location.lng());

            var bubbleContent = $("<div/>");
            if (title) {
                bubbleContent.append($("<h3/>").text(title));
            }

            bubbleContent
					.append($("<address/>")
						.append($("<span class='fn org'/>"))
						.append($("<span class='street-address'>").text(formattedAddress)));

            if (link) {
                bubbleContent
						.append($("<a class='info'>Get directions</a>")
							.attr({ href: link }));
            }

            marker.bubble = new TroegsBubble(map, this, bubbleContent, false);
        });

        google.maps.event.addListener(marker, "dblclick", function() {
            this.map.setCenter(marker.position);
            this.map.setZoom(this.map.getZoom() + 1);
        });

        return marker;
    }

    function TroegsBubble(map, marker, content, bindLinkClick) {
        this.map = map;
        this.marker = marker;
        this.content = content.clone();

        // Open a lightbox when the "more info" link is clicked.
        if (bindLinkClick !== false) {
            this.content.find("a").click(TB_bind);
        }

        this.bubble = TroegsBubble.getBubble();
        this.shadow = TroegsBubble.getShadow();
        this.visible = true;

        this.fillBubble();
        this.setMap(map);
    }

    TroegsBubble.bubble = $("div.bubble").remove();
    TroegsBubble.getBubble = function() {
        return TroegsBubble.bubble.clone();
    };

    TroegsBubble.shadow = $("div.bubble_shadow").remove();
    TroegsBubble.getShadow = function() {
        return TroegsBubble.shadow.clone();
    };

    TroegsBubble.prototype = $.extend(new google.maps.OverlayView(), {
        bubbleAppended: false,

        close: function() {
            this.visible = false;
            this.bubble.remove();
            this.shadow.remove();
        },

        draw: function() {

            // Don't do anything if the bubble is hidden.
            if (!this.visible) {
                return;
            }

            // Append the overlay to the map if it's not already there.
            if (!this.bubbleAppended) {
                this.addBubble();
            }

            // Position the overlay over the clicked marker.
            var markerPosition = this.getProjection().fromLatLngToDivPixel(this.marker.position),
				position = {
				    top: markerPosition.y - this.bubble.height() - 30,
				    left: markerPosition.x - Math.floor(this.bubble.width() / 2)
				};

            this.bubble.css(position);
            this.shadow.css(position);

            // Append the overlay to the map if it's not already there.
            if (!this.bubbleAppended) {
                this.moveBubbleIntoView();
                this.bubbleAppended = true;
            }
        },

        remove: function() {
            this.close();
        },

        // Move the map so the bubble is in view if it's outside of the viewport.
        moveBubbleIntoView: function() {

            // Adding 20px of padding around the bubble.
            var bubbleBounds = this.getBubbleBounds(),
        neLatLng = this.getProjection().fromDivPixelToLatLng(new google.maps.Point(bubbleBounds.right, bubbleBounds.top)),
        swLatLng = this.getProjection().fromDivPixelToLatLng(new google.maps.Point(bubbleBounds.left, bubbleBounds.bottom));

            this.setMapCenterContainingBounds(new google.maps.LatLngBounds(swLatLng, neLatLng));
        },

        getBubbleBounds: function() {
            var top = parseInt(this.bubble.css("top"), 10) - 20,
        left = parseInt(this.bubble.css("left"), 10) - 20;

            return {
                top: top,
                left: left,
                bottom: top + this.bubble.height() + 40,
                right: left + this.bubble.width() + 40
            };
        },

        setMapCenterContainingBounds: function(latLngBounds) {
            var newCenter = this.map.getCenter(),
        mapBounds = this.map.getBounds(),
        neLatLng = latLngBounds.getNorthEast(),
        swLatLng = latLngBounds.getSouthWest();

            // Check if the bounds extends off the north or east sides.
            if (!mapBounds.contains(neLatLng)) {
                var neMapLatLng = mapBounds.getNorthEast(),
          neLat = newCenter.lat(),
          neLng = newCenter.lng();

                if (neLatLng.lat() > neMapLatLng.lat()) {
                    neLat += neLatLng.lat() - neMapLatLng.lat();
                }

                if (neLatLng.lng() > neMapLatLng.lng()) {
                    neLng += neLatLng.lng() - neMapLatLng.lng();
                }

                newCenter = new google.maps.LatLng(neLat, neLng);
            }

            // Check if the bounds extends off the south or west sides.
            if (!mapBounds.contains(swLatLng)) {
                var swMapLatLng = mapBounds.getSouthWest(),
          swLat = newCenter.lat(),
          swLng = newCenter.lng();

                if (swLatLng.lat() < swMapLatLng.lat()) {
                    swLat += swLatLng.lat() - swMapLatLng.lat();
                }

                if (swLatLng.lng() < swMapLatLng.lng()) {
                    swLng += swLatLng.lng() - swMapLatLng.lng();
                }

                newCenter = new google.maps.LatLng(swLat, swLng);
            }

            this.map.setCenter(newCenter);
        },

        addBubble: function() {

            // Add the bubble
            this.getPanes().floatPane.appendChild(this.bubble[0]);
            this.getPanes().floatShadow.appendChild(this.shadow[0]);

            // Make the bubble visible.
            this.bubble.show();
            this.shadow.show();

            // Wire-up the close button.
            var thisBubble = this;
            this.bubble.find(".bubble_close").click(function(e) {
                e.preventDefault();
                thisBubble.remove();
            });
        },

        fillBubble: function() {
            this.bubble.find(".bubble_content").empty().append(this.content);
        }
    });
})(jQuery);
