(function () {
  //
  // Set up namespaces and config options
  //

  /**
   * @namespace PT Global PhotoTours javascript namespace 
   */
  var PT = window.PT || {};
  window.PT = PT;

  /**
   * @namespace Gmap Google map related functions live here
   */
  PT.Gmap = {
    // Bootstrap our Google maps stuff by specifying map types, which is our
    // primary user configurable parameter
    /**
     * @const Array indexed mapping from our mapType configuration to Google's
     */
    MAP_TYPES: [
      google.maps.MapTypeId.ROADMAP,
      google.maps.MapTypeId.HYBRID
    ],

    /**
     * @const MAP_TYPE Configure this by client by indexing off of 
     * PT.GMap.MAP_TYPES. Default is set to HYBRID
     */
    MAP_TYPE: google.maps.MapTypeId.HYBRID
  };

  //
  // END Set up namespaces and config options
  //

  /**
   * @namespace XMLListings PhotoTours XML listing methods
   */
  PT.XML = {
    getValue: function (node, tag) {
      var element = node.getElementsByTagName(tag);
      if (element.length > 0 && element[0] && element[0].firstChild) {
        return element[0].firstChild.data;
      }
      return false;
    },

    getThumbValue: function (node, tag) {
      var element = node.getElementsByTagName(tag);
      element = element[0].getElementsByTagName("name");
      if (element.length > 0 && element[0] && element[0].firstChild) {
        return element[0].firstChild.data;
      }
      return false;
    }
  };

  /**
   * @namespace MarkerTemplate HTML template for listing marker popups
   */
  PT.MarkerTemplate = {
    template: null,

    process: function (node, propClass) {
      return this.template.replace(/\{(\w*)\}/g, function(str, name) { 
        return PT.XML.getValue(node, name); 
      });
    }
  };

  /**
   * The workhorse functions for running Google Maps and parsing our listings
   * data lives here.
   * @lends PT.GMap
   */
  Object.extend(PT.Gmap, {
    /**
     * Density calculation's acceptable number of standard deviations to
     * calculate clusters by
     */
    DENSITY_STD_DEVS: 1,

    /**
     * Default zoom level to use on the map
     */
    ZOOM_LEVEL: 12,

    /**
     * Center the map directly over SB inititally
     */
    DEFAULT_CENTER: new google.maps.LatLng(34.41, -119.71),

    /**
     * Whether or not we have set the center of the map yet
     */
    hasSetCenter: false,

	/**
	 * Collection of markers that have been added to the map
	 */
    markers: [],

    /**
     * Initialize the map, load it into the DOM element, apply our default
     * settings to it, center it, and populate it with markers
     */
    initialize: function (options) {
      if (!options.mapid) { throw Error('Missing argument: mapid'); }
      if (!options.xmlUrl) { throw Error('Missing argument: xmlUrl'); }
      this.resultSet = options.resultSet ? options.resultSet : false; 

      var mapOptions = $H({
        lat: options.centerLat  || false,
        lng: options.centerLong || false
      });

      // Parse options
      if (options.mapOptions) { mapOptions.update(options.mapOptions); }
      this.parseOptions(mapOptions);

      // Get the map element
      var el  = document.getElementById(options.mapid);

      // Create the map
      this.map = new google.maps.Map(el, this.options);

      // Populate with markers
	  this.hasSetCenter = false;
      this.populateGoogleMap(options.xmlUrl);

      return this.map;
    },

    /**
     * Parse options, providing defafult options when none are provided
     * Configurable options are zoom level, center, and map type
     */
    parseOptions: function (options) {
      var defaults = $H({
        zoom      : PT.Gmap.ZOOM_LEVEL,
        center    : PT.Gmap.DEFAULT_CENTER,
        mapTypeId : PT.Gmap.MAP_TYPE,
        scrollwheel: false,
        backgroundColor: '#cccccc',
        mapTypeControlOptions: {
          position: google.maps.ControlPosition.TOP_RIGHT
        }
      });

      if (!PT.Gmap.hasSetCenter && (options.lat && options.lng)) {
        defaults.center = new google.maps.LatLng(options.lat, options.lng);
        PT.Gmap.hasSetCenter = true;
      }

      this.options = defaults.merge(options).toObject();
      return this.options;
    },

    /**
     * Make the ajax request that grabs the XML representation of the current
     * search results. Delegates out to #plotListings to parse the response
     * and plot the listings in the map
     */
    populateGoogleMap: function (xmlUrl) {
      if (!xmlUrl) { return; }
      PT.Gmap.currentXmlUrl = xmlUrl;
      var that = this;
      var request = new Ajax.Request(xmlUrl, {
        method: 'get',
        onSuccess: this.plotListings.bind(that)
      });
    },

    /**
     * Called when search results are loaded as a result of advancing between
     * pages of search results
     */
    repopulateGoogleMap: function (xmlUrl) {
      this.populateGoogleMap(xmlUrl);
    },

    /**
     * Parse the xml response from the ajax request and construct markers
     * for each listing
     */
    plotListings: function (transport) {
	  var listings = this.getListings(transport);
      this.updateResultSetInfo();

      this.markers.clearAll();
      listings.each(this.addListingMarker.bind(this));

      // Now that we have markers on the map, we can center it to the
      // optimal spot
	  if (this.hasSetCenter === false) {
		this.centerMap();
	  }

      if (nextURL) {
        this.populateGoogleMap(decodeURI(nextURL));
      }
    },

	addListingMarker: function (xmlMarker, i) {
	  var ptMarker = PT.Gmap.Marker(xmlMarker, this._lastXMLResponse);
	  if (ptMarker) { this.markers.add(ptMarker); }
	},

	getListings: function (transport) {
	  this._lastXMLResponse = this.parseXMLResponse(transport.responseXML);
	  return $A(this._lastXMLResponse.listings);
	},

    updateResultSetInfo: function () {
      if (this.resultSet === false) { return; }
      this.resultSet.update({
        numResults: this._lastXMLResponse.totalCount
      });
      this.resultSet.updateMessageArea();
      this.resultSet.updateSortingState();
    },

    /**
     * Wrestle the xml data out to something we can use.
     */
    parseXMLResponse: function (xml) {
      var self = {
        nextURL     : PT.XML.getValue(xml.documentElement, "nextURL"),
        propClass   : xml.documentElement.getAttribute("propClass"),
        totalCount  : xml.documentElement.getAttribute("total"),
        listings    : xml.documentElement.getElementsByTagName("listing"),
        template    : PT.XML.getValue(xml.documentElement, "template")
      };
      PT.MarkerTemplate.template = self.template;
      return self;
    },

    /**
     * Center the map after tiles have loaded in the map, but do this only
     * once. map.isLoaded is no longer in the v3 api, so using the tilesloaded
     * event is the best way to achieve this.
     */
    centerMap: function () {
      var that = this, listener;
      if (this.markers.length > 1) {
        point = this.markers.highestDensityPosition();
      } else {
        point = this.markers[0].getPosition();
      }

      this.map.setZoom(PT.Gmap.ZOOM_LEVEL);
      this.map.panTo(point);
	  this.hasSetCenter = true;
    },

    /**
     * Constructs a google maps marker, we wrap this so that we preform
     * some initial work before returning our marker in a nice clean object
     * @class Marker
     * @returns google.maps.Marker
     * @constructor
     */
    Marker: function (xml, metadata) {
      var data = PT.Gmap.Marker.parseXML(xml, metadata);

      if (!PT.Gmap.Marker.isValidLatLng(data)) {
        return false;
      }

      var point  = new google.maps.LatLng(data.lat, data.lng);
      var marker = new google.maps.Marker({
        position: point,
        icon: '/images/gmap-marker.png'
      });

      google.maps.event.addListener(marker, "click", function () {
        marker.infoWindow = new google.maps.InfoWindow({content: data.html});
        marker.infoWindow.open(PT.Gmap.map, marker);
      });

      return marker;
    }
  });

  /**
   * Marer class helper functions
   * @lends PT.Gmap.Marker
   */
  Object.extend(PT.Gmap.Marker, {
    /**
     * Parses listing specific XML to pull out the data we need to plot
     * markers
     * @private
     */
    parseXML: function (xml, metadata) {
      return {
        lat  : parseFloat(PT.XML.getValue(xml, "latitude")),
        lng  : parseFloat(PT.XML.getValue(xml, "longitude")),
        html : PT.MarkerTemplate.process(xml,  metadata.propClass),
        address: PT.XML.getValue(xml, "address") + ", " + PT.XML.getValue(xml, "zip")
      };
    },

    /**
     * Determine if the position data we've been given is plotable
     * @private
     */
    isValidLatLng: function (data) {
      return (data.lat && (data.lat > 20   && data.lat < 50)) && 
             (data.lng && (data.lng < -100 && data.lng > -150));
    }
  });

  /**
   * Aggregate functions for markers collection: PT.Gmap.markers
   * @lends PT.Gmap.markers.prototype
   */
  Object.extend(PT.Gmap.markers, {
    /**
     * Delete all markers from the map and empty our markers collection
     */
    clearAll: function () {
      var markers = this.compact(), 
          len = this.length;
      for (var i = 0; i < len; ++i) {
        markers[i].setMap(null);
        if (markers[i].infoWindow) {
          markers[i].infoWindow.close();
        }
      }
      this.length = 0;
      return this;
    },

    /**
     * Adds the marker to the collection and populates it in the map
     */
    add: function (marker) {
      this.push(marker);
      marker.setMap(PT.Gmap.map);
      return this;
    },

    /**
     * Calculate the latitude and longitude with the highest
     * density of markers
     * @returns google.maps.LatLng object Google object for latitude and longitude
     */
    highestDensityPosition: function () {//{{{
      var sums = { x: 0, y: 0, count: 0, x_sqr: 0, y_sqr: 0 };
          markers = PT.Gmap.markers;

      markers.each(function (elem, i) {
        sums.x += elem.position.lng();
        sums.y += elem.position.lat();
        sums.count += 1;
      });

      sums.mx = sums.x/sums.count;
      sums.my = sums.y/sums.count;

      markers.each(function (elem, i) {
        sums.x_sqr += Math.pow(sums.mx - elem.position.lng(), 2);
        sums.y_sqr += Math.pow(sums.my - elem.position.lat(), 2);
      });

      // Standard deviation
      sums.sx = Math.sqrt(sums.x_sqr/sums.count);
      sums.sy = Math.sqrt(sums.y_sqr/sums.count);

      cluster = [];
      markers.each(function (elem, i) {
        var devs = PT.Gmap.DENSITY_STD_DEVS;
        var lng = elem.position.lng();
        var lat = elem.position.lat();
        var inXBound = (lng >= (sums.mx - sums.sx * devs) && lng <= (sums.mx + sums.sx * devs)),
            inYBound = (lat >= (sums.my - sums.sy * devs) && lat <= (sums.my + sums.sy * devs));
        if (!(inXBound && inYBound)) { return; }
        cluster.push(elem);
      });

	  var avg = { x: 0, y: 0, count: 0 };
      if (cluster.length > 0) {
        cluster.each(function (elem, i) {
          var lng = elem.position.lng();
          var lat = elem.position.lat();
          avg.x += lng;
          avg.y += lat;
          avg.count += 1;
        });
      }

      return new google.maps.LatLng(avg.y/avg.count, avg.x/avg.count);
    }//}}}
  });
})();

