/**
 * Listings RunTrackr Map.
 *
 * Defines the map to be used for displaying listings of routes when, for
 * example, browsing or finding routes.
 *
 * @class ListingMap
 * @namespace runtrackr
 * @requires runtrackr.Map
 */

/**
 * Shows a GInfoWindow at the marker with its description as the contents.
 *
 * Note that this extends the GMarker object/class.  It is included with
 * this class because it will be used by its methods.
 */
GMarker.prototype.popupInfoWindow = function()
{
  this.openInfoWindowHtml(this.routeDescription, {maxWidth: '300'});
}

/**
 * Constructor must take the id of the element to use as the map, along with
 * optional parameters specifying the initial map state.
 *
 * @param Object options an object containing the following properties:
 *        mapId: (String) the id of the DOM element to use as the map.
 *        centerPoint: (GLatLng) the optional initial location to center on.
 *        initialZoom: (Number) the optional initial zoom level of the map.
 *        ajaxLoadRoutesUrl: (String) the base URL to retrieve route listings
 *          from.
 *        viewRouteUrl: (String) used for linking to a full view of the route.
 */
runtrackr.ListingMap = function(options)
{
  runtrackr.Map.apply(this, arguments);

  // Set URLs.
  this._ajaxLoadRoutesUrl = options.ajaxLoadRoutesUrl;
  this._viewRouteUrl = options.viewRouteUrl;

  this._mapsApiKey = options.mapsApiKey ? options.mapsApiKey : false;
}
runtrackr.ListingMap.prototype = new runtrackr.Map();

// TODO: Test that `undefined` is returned from void function calls
// http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Statements:return
/**
 * Finds the list of routes near the specified location and displays them
 * on the map.
 *
 * If a single, unambiguous location cannot be found, the user is prompted
 * to narrow the search or try again.
 *
 * @param String locationValue the location to search for nearby routes.
 * @param Object callback a set of callback functions {success, failure} to
 *        invoke once the request has completed.
 */
runtrackr.ListingMap.prototype.findRoutes = function(locationValue, callback)
{
  // TODO: How to combine this with the geocoder lookup in RouteMap?

  // Used by the callback to gain access to the instance object by forming
  // a closure.
  var this_ = this;
  this._geocoder.getLocations(
    locationValue,
    function(response)
    {
      // Call the actual Geocoder callback function with the proper context.
      this_._findRoutesGeocoderCallback(response, callback);
    }
  );
}

/**
 * Callback function to deal with the geocoder response.
 *
 * This is defined independently so that it can be called in the context
 * of a ListingMap object, so that object members/fields will be available.
 *
 * @param Object response the geocoder response containing a list of places.
 * @param Object callback the callback functions. ({success, failure}).
 * @private
 */
runtrackr.ListingMap.prototype._findRoutesGeocoderCallback = function(response, callback)
{
  // TODO: Use try-catch to clean up the flow...
  if (response.Status.code == G_GEO_SUCCESS)
  {
    // Geocoder was able to resolve the location to a lat/lng.
    if (response.Placemark.length == 1)
    {
      // Ensure that there was one unique location returned.
      var place = response.Placemark[0];

      // Extract the coordinates and build a GLatLng point.
      var lng = place.Point.coordinates[0];
      var lat = place.Point.coordinates[1];
      var point = new GLatLng(lat, lng);

      // Set zoom level based on accuracy returned.
      var zoomLevel = this._getZoomLevel(place.AddressDetails.Accuracy);

      // Move map to proper location.
      this._map.setCenter(point, zoomLevel);

      // Get the list of routes near this location and display them on the map.
      this._loadRoutes(point, zoomLevel, callback.success);
    }
    else
    {
      // TODO: May want to have a separate property/function specifically to
      // deal with this to avoid having if/else in the failure callback.

      // Multiple matches found for the query; pass the list of matches back
      // to the callback to process.
      if (typeof callback.failure == 'function')
      {
        callback.failure(runtrackr.Map.ERROR.LOCATION_MULTIPLE_MATCHES,
          response.Placemark
        );
      }
    }
  }
  else
  {
    // Could not resolve the location to a lat/lng point.
    if (typeof callback.failure == 'function')
      callback.failure(runtrackr.Map.ERROR.LOCATION_NOT_FOUND);
  }
}

/**
 * Gets the map zoom level associated with a given accuracy returned from the
 * geocoder.
 *
 * This specifies the recommended zoom level to set the map to for a given
 * accuracy response from the geocoder.  For example, the zoom level is
 * higher for street-level accuracy than for country-level accuracy.
 *
 * @param Number accuracy the accuracy level, as defined in
 * {@link runtrackr.Map.GGeoAddressAccuracy}
 * @return the zoom level corresponding to the supplied accuracy.
 * @private
 */
runtrackr.ListingMap.prototype._getZoomLevel = function(accuracy)
{
  // TODO: Tailor this zoom level properly... should be different
  // than for add-route, since don't want hyper-zoom levels even
  // if accuracy is high; want routes in general vicinity.
  // USE CONSTANTS.
  var zoomLevel = 5;

  if (accuracy == 8)
  {
    zoomLevel = 16;
  }
  else if (accuracy >= 6)
  {
    zoomLevel = 15;
  }
  else if (accuracy >= 4)
  {
    zoomLevel = 13;
  }

  return zoomLevel;
}

/**
 * Loads routes near the specified points and displays them on the screen.
 *
 * The search radius is determined by the zoom.  The exact details are
 * determined server-side, as is the limit on the number of routes returned.
 *
 * @param GLatLng point the coordinates to search for nearby routes.
 * @param Number zoom the zoom level, which determines the search radius.
 * @param Function callback the success callback.  It will be passed an array
 *        of the routes found.
 * @private
 */
runtrackr.ListingMap.prototype._loadRoutes = function(point, accuracy, callback)
{
  var ajaxUrl = this._ajaxLoadRoutesUrl +
    '/' + point.lat() + '/' + point.lng() + '/' + accuracy;

  // TODO: Figure out how to call the handlers using `this` as the context
  // without creating closures... any JS libraries for this?
  var this_ = this;

  // Execute Ajax query to get the list of routes from the server.
  jQuery.ajax(
  {
    'type' : 'GET',
    'url' : ajaxUrl,
    'dataType' : 'json',
    'success' : function(responseData, textStatus)
    {
      // Call the actual event handler with the proper context.
      // TODO: This could be a property of `runtrackr.ListingMap.prototype.loadRoutes`,
      // but then need to use `call` to set the context to `this_` and NOT
      // the `loadRoutes` function object!
      this_._loadRoutesSuccessHandler(responseData, textStatus, callback);
    },
    'error' : function(XMLHttpRequest, textStatus, errorThrown)
    {
      // TODO: Define what to show here.
      window.alert(XMLHttpRequest.responseText);
    }
  });
}

/**
 * Ajax load routes success handler.
 *
 * @param Object responseData the response data in JSON.
 * @param String textStatus the status.
 * @param Function callback the success callback.
 * @private
 */
runtrackr.ListingMap.prototype._loadRoutesSuccessHandler = function(responseData, textStatus, callback)
{
  var routes = responseData.routes;

  // Clear current results/map state.
  this._map.clearOverlays();
  this._markers = [];

  // Create a marker for each route returned.
  for (var i in routes)
  {
    // Create the marker and attach a description to it.
    var route = routes[i];
    var routeMarker = new GMarker(new GLatLng(route.lat, route.lng));
    var routeUrlFragment = route.slug ? route.slug : route.id;
    var routeUrl = this._viewRouteUrl + '/' + routeUrlFragment;
    var routeDistance = parseFloat(route.distance);

    // Make a copy/clone of the template, add in the proper information and
    // then set it to be the description in the marker's infoWindowHtml
    var infoWindowTemplate = jQuery('.info-window.template').clone();
    infoWindowTemplate.find('.name').attr('href', routeUrl).text(route.name);
    infoWindowTemplate.find('.distance .value').text(routeDistance.toFixed(2));
    infoWindowTemplate.find('.description .value').text(route.description);
    infoWindowTemplate.find('.preview a').attr('href', routeUrl);
    infoWindowTemplate.find('.preview img').attr('src',
      runtrackr.StaticMap.getImageUrl(route.encodedPoints, route.encodedLevels, {size: '250x250', zoom: '12'}));

    // TODO: Fix until decide whether to use per-use distance units settings.
    infoWindowTemplate.find('.distance .miles.value')
      .text((routeDistance/runtrackr.DISTANCE_UNITS.CONVERSIONS.MILES_TO_KMS).toFixed(2));

    routeMarker.routeDescription = infoWindowTemplate.html();


    // TODO: Also have the selected route in the side bar be highlighted when
    // directly clicking a marker here! (Vice-versa)


    // Show info window about route when marker clicked.  Note that
    // #popupInfoWindow() is a custom function that has been added to GMarker.
    GEvent.addListener(routeMarker, "click", routeMarker.popupInfoWindow);

    // Add marker to the map.
    this._map.addOverlay(routeMarker);

    // TODO: When using this associative array, order does not matter.
    // Will we need to use order, e.g. ordered by routes closest to map?
    // TODO: Also, this may not be proper use of an array, e.g. we are just
    // adding properties to it, this may not increase the length property.
    // http://blog.persistent.info/2004/08/javascript-associative-arrays.html
    this._markers['route-id-' + route.id] = routeMarker;
  }

  // Trigger callback function.
  if (typeof callback == 'function')
  {
    callback(routes);
  }
}