/**
 * @name InfoBoxOptions
 * @class This class represents the optional parameter passed to the {@link InfoBox} constructor.
 * @property {string|Node} content The content of the InfoBox (plain text or an HTML DOM node).
 * @property {boolean} disableAutoPan Disable auto-pan on <tt>open</tt> (default is <tt>false</tt>).
 * @property {number} maxWidth The maximum width (in pixels) of the InfoBox. Set to 0 if no maximum.
 * @property {Size} pixelOffset The offset (in pixels) from the top left corner of the InfoBox
 *  to the map pixel corresponding to <tt>position</tt>.
 * @property {LatLng} position The geographic location at which to display the InfoBox.
 * @property {number} zIndex The CSS z-index style value for the InfoBox.
 *  Note: This value overrides a zIndex setting specified in the <tt>boxStyle</tt> property.
 * @property {Object} boxStyle An object literal with CSS style settings for the InfoBox container.
 *  Note: Border widths must be specified in px units because of an MSIE limitation.
 * @property {string} closeBoxMargin The CSS margin style value for the close box.
 *  The default is "2px" (a 2-pixel margin on all sides).
 * @property {string} closeBoxURL The URL of the image representing the close box.
 *  Note: The default is the URL for Google's standard close box.
 *  Set this property to "" if no close box is required.
 * @property {Size} infoBoxClearance Minimum offset (in pixels) from the InfoBox to the
 *  map edge after an auto-pan.
 * @property {boolean} isHidden Hide the InfoBox on <tt>open</tt> (default is <tt>false</tt>).
 * @property {string} pane The pane where the InfoBox is to appear (default is "floatPane").
 *  Set the pane to "mapPane" if the InfoBox is being used as a map label.
 *  Valid pane names are the property names for the <tt>google.maps.MapPanes</tt> object.
 * @property {boolean} enableEventPropagation Propagate mousedown, click, dblclick,
 *  and contextmenu events in the InfoBox (default is <tt>false</tt> to mimic the behavior
 *  of a <tt>google.maps.InfoWindow</tt>). Set this property to <tt>true</tt> if the InfoBox
 *  is being used as a map label. iPhone note: This property setting has no effect. Events are
 *  always propagated for an InfoBox in the "mapPane" pane and they are <i>not</i> propagated
 *  for an InfoBox in the "floatPane" pane.
 */

/**
 * Creates an InfoBox with the options specified in {@link InfoBoxOptions}.
 *  Call <tt>InfoBox.open</tt> to add the box to the map.
 * @constructor
 * @param {InfoBoxOptions} [opt_opts]
 */
function InfoBox(opt_opts) {

	opt_opts = opt_opts || {};

	google.maps.OverlayView.apply(this, arguments);

	// Standard options (in common with google.maps.InfoWindow):
	//
	this.content_ = opt_opts.content || "";
	this.disableAutoPan_ = opt_opts.disableAutoPan || false;
	this.maxWidth_ = opt_opts.maxWidth || 0;
	this.pixelOffset_ = opt_opts.pixelOffset || new google.maps.Size(0, 0);
	this.position_ = opt_opts.position || new google.maps.LatLng(0, 0);
	this.zIndex_ = opt_opts.zIndex || null;

	// Additional options (unique to InfoBox):
	//
	this.boxStyle_ = opt_opts.boxStyle || {};
	this.closeBoxMargin_ = opt_opts.closeBoxMargin || "2px";
	this.closeBoxURL_ = opt_opts.closeBoxURL
			|| "http://www.google.com/intl/en_us/mapfiles/close.gif";
	if (opt_opts.closeBoxURL === "") {
		this.closeBoxURL_ = "";
	}
	this.infoBoxClearance_ = opt_opts.infoBoxClearance
			|| new google.maps.Size(1, 1);
	this.isHidden_ = opt_opts.isHidden || false;
	this.pane_ = opt_opts.pane || "floatPane";
	this.enableEventPropagation_ = opt_opts.enableEventPropagation || false;

	this.div_ = null;
	this.closeListener_ = null;
	this.eventListener1_ = null;
	this.eventListener2_ = null;
	this.eventListener3_ = null;
	this.contextListener_ = null;
	this.fixedWidthSet_ = null;
	
	this.keepOpen_ = false;
}

/* InfoBox extends OverlayView in the Google Maps API v3.
 */
InfoBox.prototype = new google.maps.OverlayView();

/**
 * Creates the DIV representing the InfoBox.
 * @private
 */
InfoBox.prototype.createInfoBoxDiv_ = function() {

	var bw;
	var me = this;

	// This handler prevents an event in the InfoBox from being passed on to the map.
	//
	var cancelHandler = function(e) {
		e.cancelBubble = true;

		if (e.stopPropagation) {

			e.stopPropagation();
		}
	};

	// This handler ignores the current event in the InfoBox and conditionally prevents
	// the event from being passed on to the map. It is used for the contextmenu event.
	//
	var ignoreHandler = function(e) {

		e.returnValue = false;

		if (e.preventDefault) {

			e.preventDefault();
		}

		if (!me.enableEventPropagation_) {

			cancelHandler(e);
		}
	};

	if (!this.div_) {

		this.div_ = document.createElement("div");

		this.setBoxStyle_();

		// Apply required styles:
		//
		this.div_.style.position = "absolute";
		this.div_.style.visibility = 'hidden';

		
		
		if (this.zIndex_ !== null) {

			this.div_.style.zIndex = this.zIndex_;
		}

		if (typeof this.content_.nodeType === "undefined") {
			this.div_.innerHTML = this.getCloseBoxImg_() + this.content_;
		} else {
			this.div_.innerHTML = this.getCloseBoxImg_();
			this.div_.appendChild(this.content_);
		}

		// Add the InfoBox DIV to the DOM
		this.getPanes()[this.pane_].appendChild(this.div_);

		this.addClickHandler_();

		if (this.div_.style.width) {

			this.fixedWidthSet_ = true;

		} else {

			if (this.maxWidth_ !== 0 && this.div_.offsetWidth > this.maxWidth_) {

				this.div_.style.width = this.maxWidth_;
				this.div_.style.overflow = "auto";
				this.fixedWidthSet_ = true;

			} else { // The following code is needed to overcome problems with MSIE

				bw = this.getBoxWidths_();

				this.div_.style.width = (this.div_.offsetWidth - bw.left - bw.right)
						+ "px";
				this.fixedWidthSet_ = false;
			}
		}

		this.panBox_(this.disableAutoPan_);

		if (!this.enableEventPropagation_) {

			// Cancel event propagation.
			//
			this.eventListener1_ = google.maps.event.addDomListener(this.div_,
					"mousedown", cancelHandler);
			this.eventListener2_ = google.maps.event.addDomListener(this.div_,
					"click", cancelHandler);
			this.eventListener3_ = google.maps.event.addDomListener(this.div_,
					"dblclick", cancelHandler);
		}

		this.contextListener_ = google.maps.event.addDomListener(this.div_,
				"contextmenu", ignoreHandler);

		/**
		 * This event is fired when the DIV containing the InfoBox's content is attached to the DOM.
		 * @name InfoBox#domready
		 * @event
		 */
		google.maps.event.trigger(this, "domready");
	}
};

/**
 * Returns the HTML <IMG> tag for the close box.
 * @private
 */
InfoBox.prototype.getCloseBoxImg_ = function() {
	var img = "";

	if (this.closeBoxURL_ !== "") {
        var img = '<div class="map_bubble_close"></div>';
    }
	
	return img;
};

/**
 * Adds the click handler to the InfoBox close box.
 * @private
 */
InfoBox.prototype.addClickHandler_ = function() {

	var closeBox;

	if (this.closeBoxURL_ !== "") {

		closeBox = this.div_.firstChild;
		this.closeListener_ = google.maps.event.addDomListener(closeBox,
				'click', this.getCloseClickHandler_());

	} else {
		this.closeListener_ = null;
	}
};

/**
 * Returns the function to call when the user clicks the close box of an InfoBox.
 * @private
 */
InfoBox.prototype.getCloseClickHandler_ = function() {

	var me = this;

	return function() {

		me.close();

		/**
		 * This event is fired when the InfoBox's close box is clicked.
		 * @name InfoBox#closeclick
		 * @event
		 */
		google.maps.event.trigger(me, "closeclick");
	};
};

/**
 * Pans the map so that the InfoBox appears entirely within the map's visible area.
 * @private
 */
InfoBox.prototype.panBox_ = function(disablePan) {

	if (!disablePan) {

		var map = this.getMap();
		var bounds = map.getBounds();

		// The degrees per pixel
		var mapDiv = map.getDiv();
		var mapWidth = mapDiv.offsetWidth;
		var mapHeight = mapDiv.offsetHeight;
		var boundsSpan = bounds.toSpan();
		var longSpan = boundsSpan.lng();
		var latSpan = boundsSpan.lat();
		var degPixelX = longSpan / mapWidth;
		var degPixelY = latSpan / mapHeight;

		// The bounds of the map
		var mapWestLng = bounds.getSouthWest().lng();
		var mapEastLng = bounds.getNorthEast().lng();
		var mapNorthLat = bounds.getNorthEast().lat();
		var mapSouthLat = bounds.getSouthWest().lat();

		// The bounds of the box
		var position = this.position_;
		var iwOffsetX = this.pixelOffset_.width;
		var iwOffsetY = this.pixelOffset_.height;
		var padX = this.infoBoxClearance_.width;
		var padY = this.infoBoxClearance_.height;
		var iwWestLng = position.lng() + (iwOffsetX - padX) * degPixelX;
		var iwEastLng = position.lng()
				+ (iwOffsetX + this.div_.offsetWidth + padX) * degPixelX;
		// Changed to take into account change where infobox bottom aligns with marker bottom
		var iwNorthLat = position.lat()
				+ (iwOffsetY + this.div_.offsetHeight + 15) * degPixelY;
		var iwSouthLat = position.lat() + (iwOffsetY - padY - 5) * degPixelY;

		// Calculate center shift
		var shiftLng = (iwWestLng < mapWestLng ? mapWestLng - iwWestLng : 0)
				+ (iwEastLng > mapEastLng ? mapEastLng - iwEastLng : 0);
		var shiftLat = (iwNorthLat > mapNorthLat ? mapNorthLat - iwNorthLat : 0)
				+ (iwSouthLat < mapSouthLat ? mapSouthLat - iwSouthLat : 0);

		if (!(shiftLat === 0 && shiftLng === 0)) {

			// Move the map to the new shifted center.
			//
			var c = map.getCenter();
			map.setCenter(new google.maps.LatLng(c.lat() - shiftLat, c.lng()
					- shiftLng));
		}
	}
};

/**
 * Sets the style of the InfoBox.
 * @private
 */
InfoBox.prototype.setBoxStyle_ = function() {

	var i;

	var boxStyle = this.boxStyle_;

	for (i in boxStyle) {

		if (boxStyle.hasOwnProperty(i)) {

			this.div_.style[i] = boxStyle[i];
		}
	}

	// Fix up opacity style for benefit of MSIE:
	//
	if (typeof this.div_.style.opacity !== "undefined" && this.div_.style.opacity !== "") {
		this.div_.style.filter = "alpha(opacity=" + (this.div_.style.opacity * 100) + ")";
	}
};

/**
 * Get the widths of the borders of the InfoBox.
 * @private
 * @return {Object} widths object (top, bottom left, right)
 */
InfoBox.prototype.getBoxWidths_ = function() {

	var computedStyle;
	var bw = {
		top : 0,
		bottom : 0,
		left : 0,
		right : 0
	};
	var box = this.div_;

	if (document.defaultView && document.defaultView.getComputedStyle) {

		computedStyle = box.ownerDocument.defaultView.getComputedStyle(box, "");

		if (computedStyle) {

			// The computed styles are always in pixel units (good!)
			bw.top = parseInt(computedStyle.borderTopWidth, 10) || 0;
			bw.bottom = parseInt(computedStyle.borderBottomWidth, 10) || 0;
			bw.left = parseInt(computedStyle.borderLeftWidth, 10) || 0;
			bw.right = parseInt(computedStyle.borderRightWidth, 10) || 0;
		}

	} else if (document.documentElement.currentStyle) { // MSIE

		if (box.currentStyle) {

			// The current styles may not be in pixel units, but assume they are (bad!)
			bw.top = parseInt(box.currentStyle.borderTopWidth, 10) || 0;
			bw.bottom = parseInt(box.currentStyle.borderBottomWidth, 10) || 0;
			bw.left = parseInt(box.currentStyle.borderLeftWidth, 10) || 0;
			bw.right = parseInt(box.currentStyle.borderRightWidth, 10) || 0;
		}
	}

	return bw;
};

/**
 * Invoked when <tt>close</tt> is called. Do not call it directly.
 */
InfoBox.prototype.onRemove = function() {

	if (this.div_) {

		this.div_.parentNode.removeChild(this.div_);
		this.div_ = null;
	}
};

/**
 * Draws the InfoBox based on the current map projection and zoom level.
 */
InfoBox.prototype.draw = function() {

	this.createInfoBoxDiv_();

	var pixPosition = this.getProjection().fromLatLngToDivPixel(this.position_);

	this.div_.style.left = (pixPosition.x + this.pixelOffset_.width) + "px";
	// Changed so infobox bottom aligns with marker bottom
	this.div_.style.bottom = -(pixPosition.y - 32) + 'px';
	if (this.isHidden_) {

		this.div_.style.visibility = 'hidden';

	} else {

		this.div_.style.visibility = "visible";
	}
};

/**
 * Sets the options for the InfoBox. Note that changes to the <tt>maxWidth</tt>,
 *  <tt>closeBoxMargin</tt>, <tt>closeBoxURL</tt>, and <tt>enableEventPropagation</tt>
 *  properties have no affect until the current InfoBox is <tt>close</tt>d and a new one
 *  is <tt>open</tt>ed.
 * @param {InfoBoxOptions} opt_opts
 */
InfoBox.prototype.setOptions = function(opt_opts) {

	if (typeof opt_opts.boxStyle !== "undefined") { // Must be first

		this.boxStyle_ = opt_opts.boxStyle;
		this.setBoxStyle_();
	}
	if (typeof opt_opts.content !== "undefined") {

		this.setContent(opt_opts.content);
	}
	if (typeof opt_opts.disableAutoPan !== "undefined") {

		this.disableAutoPan_ = opt_opts.disableAutoPan;
	}
	if (typeof opt_opts.maxWidth !== "undefined") {

		this.maxWidth_ = opt_opts.maxWidth;
	}
	if (typeof opt_opts.pixelOffset !== "undefined") {

		this.pixelOffset_ = opt_opts.pixelOffset;
	}
	if (typeof opt_opts.position !== "undefined") {

		this.setPosition(opt_opts.position);
	}
	if (typeof opt_opts.zIndex !== "undefined") {

		this.setZIndex(opt_opts.zIndex);
	}
	if (typeof opt_opts.closeBoxMargin !== "undefined") {

		this.closeBoxMargin_ = opt_opts.closeBoxMargin;
	}
	if (typeof opt_opts.closeBoxURL !== "undefined") {

		this.closeBoxURL_ = opt_opts.closeBoxURL;
	}
	if (typeof opt_opts.infoBoxClearance !== "undefined") {

		this.infoBoxClearance_ = opt_opts.infoBoxClearance;
	}
	if (typeof opt_opts.isHidden !== "undefined") {

		this.isHidden_ = opt_opts.isHidden;
	}

	if (typeof opt_opts.enableEventPropagation !== "undefined") {

		this.enableEventPropagation_ = opt_opts.enableEventPropagation;
	}

	if (this.div_) {

		this.draw();
	}
};

/**
 * Sets the content of the InfoBox.
 *  The content can be plain text or an HTML DOM node.
 * @param {string|Node} content
 */
InfoBox.prototype.setContent = function(content) {
	this.content_ = content;

	if (this.div_) {

		if (this.closeListener_) {

			google.maps.event.removeListener(this.closeListener_);
			this.closeListener_ = null;
		}

		// Odd code required to make things work with MSIE.
		//
		if (!this.fixedWidthSet_) {

			this.div_.style.width = "";
		}

		if (typeof content.nodeType === "undefined") {
			this.div_.innerHTML = this.getCloseBoxImg_() + content;
		} else {
			this.div_.innerHTML = this.getCloseBoxImg_();
			this.div_.appendChild(content);
		}

		// Perverse code required to make things work with MSIE.
		// (Ensures the close box does, in fact, float to the right.)
		//
		if (!this.fixedWidthSet_) {

			this.div_.style.width = this.div_.offsetWidth + "px";
			this.div_.innerHTML = this.getCloseBoxImg_() + content;
		}

		this.addClickHandler_();
	}

	/**
	 * This event is fired when the content of the InfoBox changes.
	 * @name InfoBox#content_changed
	 * @event
	 */
	google.maps.event.trigger(this, "content_changed");
};

/**
 * Sets the geographic location of the InfoBox.
 * @param {LatLng} latlng
 */
InfoBox.prototype.setPosition = function(latlng) {

	this.position_ = latlng;

	if (this.div_) {

		this.draw();
	}

	/**
	 * This event is fired when the position of the InfoBox changes.
	 * @name InfoBox#position_changed
	 * @event
	 */
	google.maps.event.trigger(this, "position_changed");
};

/**
 * Sets the zIndex style for the InfoBox.
 * @param {number} index
 */
InfoBox.prototype.setZIndex = function(index) {

	this.zIndex_ = index;

	if (this.div_) {

		this.div_.style.zIndex = index;
	}

	/**
	 * This event is fired when the zIndex of the InfoBox changes.
	 * @name InfoBox#zindex_changed
	 * @event
	 */
	google.maps.event.trigger(this, "zindex_changed");
};

/**
 * Returns the content of the InfoBox.
 * @returns {string}
 */
InfoBox.prototype.getContent = function() {

	return this.content_;
};

/**
 * Returns the geographic location of the InfoBox.
 * @returns {LatLng}
 */
InfoBox.prototype.getPosition = function() {

	return this.position_;
};

/**
 * Returns the zIndex for the InfoBox.
 * @returns {number}
 */
InfoBox.prototype.getZIndex = function() {

	return this.zIndex_;
};

/**
 * Shows the InfoBox.
 */
InfoBox.prototype.show = function() {

	this.isHidden_ = false;
	this.div_.style.visibility = "visible";
};

/**
 * Hides the InfoBox.
 */
InfoBox.prototype.hide = function() {

	this.isHidden_ = true;
	this.div_.style.visibility = "hidden";
};

/**
 * Adds the InfoBox to the specified map. If <tt>anchor</tt>
 *  (usually a <tt>google.maps.Marker</tt>) is specified, the position
 *  of the InfoBox is set to the position of the <tt>anchor</tt>.
 * @param {Map} map
 * @param {MVCObject} [anchor]
 */
InfoBox.prototype.open = function(map, anchor) {
	
	if (anchor) {

		this.position_ = anchor.getPosition();
	}

	this.setMap(map);

	if (this.div_) {

		this.panBox_();
	}
};

/**
 * Keep the infobox open when marker clicked, but not for mouseover
 */
InfoBox.prototype.keepOpen = function(value) {
	this.keepOpen_ = value;
}

/**
 * return value for mouseout
 */
InfoBox.prototype.getKeepOpen = function() {
	return this.keepOpen_;
}

/**
 * Removes the InfoBox from the map.
 */
InfoBox.prototype.close = function() {

	if (this.closeListener_) {

		google.maps.event.removeListener(this.closeListener_);
		this.closeListener_ = null;
	}

	if (this.eventListener1_) {

		google.maps.event.removeListener(this.eventListener1_);
		google.maps.event.removeListener(this.eventListener2_);
		google.maps.event.removeListener(this.eventListener3_);
		this.eventListener1_ = null;
		this.eventListener2_ = null;
		this.eventListener3_ = null;
	}

	if (this.contextListener_) {

		google.maps.event.removeListener(this.contextListener_);
		this.contextListener_ = null;
	}
	
	this.keepOpen(false);

	this.setMap(null);
};
