// Copyright 2008 Google Inc. All Rights Reserved.

// This needs the following to be defined at use time:
//   - GMaps API
//   - Global variable 'close_str' with the localized text for "[Close]"
//   - Global variable 'we_cant_find_str' with the localized text for
//     "We can't find %s."
// templates/base_generic.html provides 'close_str' and 'we_cant_find_str',
// and server/render_templates.py provides the GMaps API.

// TODO(rogerts): [post-launch] Add unit tests. Convert to Google coding style.
// TODO(rogerts): [post-launch] Do a proper code review.


var geocoder = new GClientGeocoder();
var MAPS_API_ZOOM_FOR_ACCURACY = [0, 5, 9, 11, 13, 14, 16, 17, 17];
var MAPS_API_ZOOM_FOR_EXPLICIT_COORDINATES = 14;


/**
 * Tries to parse a latitude/longitude string.
 *
 * We only accept strings of the form <number>,<number> where each number
 * matches -?[0-9]+(\.[0-9]+)?
 *
 * @param {string} text The string representation of a latitude/longitude.
 * @return {GLatLng?} A LatLng object with the extracted latitude and
 *   longitude, or "null" if "text" could not be parsed as a latitude/longitude.
 */
function maybeParseLatLng(text) {
  var pattern = /^(-?[0-9]+(?:\.[0-9]+)?)\s*,\s*(-?[0-9]+(?:\.[0-9]+)?)$/;
  match = text.match(pattern);
  if (match === null) {
    return null;
  }
  lat = parseFloat(match[1]);
  lng = parseFloat(match[2]);
  return new GLatLng(lat, lng);
}


/**
 * Builds and shows a "we cannot find this place" message.
 *
 * @param {HTMLElement} qcr DOM object for the geocoding results box.
 * @param {string} placeName The name of the place that couldn't be found.
 * @param {string} closeLinkHtml HTML for the "close box" link.
 * @param {HTMLElement} form DOM object for the geocoding form.
 */
function showGeocodeFailureMessage(qcr, placeName, closeLinkHtml, form) {
  var escapedCity = placeName.replace('<', '&lt;').replace('>', '&gt;');
  var errorMessage = we_cant_find_str.replace('%s', escapedCity);
  qcr.html(errorMessage + closeLinkHtml).show('normal', function() {
    // This callback may fix a bug where "show" doesn't work in Opera.
    $(this).css({height: '', visibility: 'visible'}); });

  var formClose = form.find('.geocoder_close');
  formClose.click(function() {
    $(this).parent().hide('normal');
    return false;
  });
}


/**
 * Builds HTML for an entry in a dropdown list, for one geocoding results.
 *
 * @param {Object} placemark A placemark element in the geocoder JSON response.
 * @param {Array.<number>} mapsApiZoomForAccuracy List that gives a map between
 *   geocoder result accuracy and the zoom level that should be used.
 * @return {string} HTML for an entry in the geociding results dropdown list.
 */
function getGeocodeDropDownListHTML(placemark, mapsApiZoomForAccuracy) {
  var p = placemark.Point;
  var address = placemark.address;
  var escapedAddress = address.replace('<', '&lt;').replace('>', '&gt;')
  var acc = placemark.AddressDetails ? placemark.AddressDetails.Accuracy : 8;
  var mapsApiZoom = mapsApiZoomForAccuracy[Math.max(Math.min(acc, 8), 0)];
  var panoramioZoom = 17 - mapsApiZoom;

  return '<li><a coords="' + p.coordinates[1] + ',' + p.coordinates[0] +
      '" href="/map/#lt=' + p.coordinates[1] + '&amp;ln=' + p.coordinates[0] +
      '&amp;z=' + panoramioZoom + '">' + escapedAddress + '</a></li>';
}


/**
 * Builds and shows a dropdown list, for multiple geocoding results.
 *
 * @param {HTMLElement} qcr DOM object for the geocoding results box.
 * @param {Object} response A geocoder JSON response.
 * @param {string} closeLinkHtml HTML for the "close box" link.
 * @param {Array.<number>} mapsApiZoomForAccuracy List that gives a map between
 *   geocoder result accuracy and the zoom level that should be used.
 */
function showGeocodeDropDownList(
    qcr, response, closeLinkHtml, mapsApiZoomForAccuracy) {
  var resultHtml = '<ul>';
  for (var i = 0; i < response.Placemark.length; i++) {
    var pm = response.Placemark[i];
    resultHtml += getGeocodeDropDownListHTML(pm, mapsApiZoomForAccuracy);
  }
  resultHtml += '</ul>' + closeLinkHtml;
  qcr.html(resultHtml).show('normal', function() {
    // This callback may fix a bug where "show" doesn't work in Opera.
    $(this).css({height: '', visibility: 'visible'}); });
}


/**
 * Handles a submission/enter event on a search box.
 *
 * Whenever the map has to be moved, the 'centerMapAtLatLong' function will
 * be called. This function is overriden multiple times in our codebase so
 * we cannot say here what it will do.
 *
 * See handleGeocoderBox for more details.
 *
 * @param {HTMLElement} form The HTML form that contains the search box.
 * @param {?function(HTMLElement)} callback If present, this callback will be
 *   bound to the onclick event of the link shown in the drop-down list; the
 *   callback will be called with the HTMLElement for the link as argument.
 */
function handleSearchBox(form, callback) {
  handleGeocoderBox(form, centerMapAtLatLong, callback);
}


/**
 * Handles a submission/enter event on a search box.
 *
 * Whenever the map has to be moved, the 'openMapAtLatLong' function will
 * be called which will open the '/map' page at the appropriate coordinates.
 * This is meant to be used by the index page.
 *
 * See handleGeocoderBox for more details.
 *
 * @param {HTMLElement} form The HTML form that contains the search box.
 */
function handleIndexPageSearchBox(form) {
  handleGeocoderBox(form, openMapPageAtLatLong, null);
}


/**
 * Handles a submission/enter event on a search box.
 *
 * The user can type a place name or a coordinate pair in the search box.
 * If he gives a coordinate pair, or a place name that resolves to a single
 * position, the map is centered to that position.
 * If it's a place name that resolves to more than one position, a drop-down
 * list is shown to let the user choose - clicking on an option will load a
 * fresh map page centered there, or to the map being recenterd.
 * If it's an unknown place name, an error message is shown.
 *
 * @param {HTMLElement} form The HTML form that contains the search box.
 * @param {function(string, GLatLng, number)} singleResultMapOpener Function to
 *   call when the map must be centered to a new position; the function will be
 *   called with the following arguments:
 *   - an empty string
 *   - position to center the map to
 *   - zoom level to use, in the Maps API scale.
 * @param {?function(HTMLElement)} callback If present, this callback will be
 *   bound to the onclick event of the link shown in the drop-down list; the
 *   callback will be called with the HTMLElement for the link as argument.
 */
function handleGeocoderBox(form, singleResultMapOpener, callback) {
  var closeLinkHtml =
      '<a href="#" style="float: right" class="geocoder_close">' +
      close_str + '</a>';
  var placeName = form.city.value;
  form = $(form);
  var qcr = form.find('.city_results');
  var mapsApiZoomForAccuracy = MAPS_API_ZOOM_FOR_ACCURACY;

  latLng = maybeParseLatLng(placeName);
  if (latLng !== null) {
    qcr.hide('normal');
    singleResultMapOpener('', latLng, MAPS_API_ZOOM_FOR_EXPLICIT_COORDINATES);
    return;
  }

  geocoder.getLocations(placeName, function(response) {
    if (!response ||
        response.Status.code != 200 ||
        response.Placemark.length == 0) {
      showGeocodeFailureMessage(qcr, placeName, closeLinkHtml, form);
    } else if (response.Placemark.length == 1) {
      var pm = response.Placemark[0];
      var coor = pm.Point.coordinates;
      var acc = pm.AddressDetails ? pm.AddressDetails.Accuracy : 8;
      var z = mapsApiZoomForAccuracy[Math.max(Math.min(acc, 8), 0)];

      qcr.hide('normal');
      singleResultMapOpener('', new GLatLng(coor[1], coor[0]), z);
    } else {
      showGeocodeDropDownList(
          qcr, response, closeLinkHtml, mapsApiZoomForAccuracy);

      if (callback) {
        qcr.find('li a').click(function() {
          var rv = callback.apply(this);
          qcr.hide('normal');
          return rv;
        });
      } else {
        qcr.find('li a').click(function() {
          qcr.hide('normal');
          return true;
        });
      }

      var formClose = form.find('.geocoder_close');
      formClose.click(function() {
        $(this).parent().hide('normal');
        return false;
      });
    }
  });
}


/**
 * Opens a fresh /map page at the desired location.
 *
 * @param {string} unused_name Ignored.
 * @param {GLatLng} pt Position to center the map on.
 * @param {number} mapsApiZoom Zoom level to use, in the Maps API scale.
 */
function openMapPageAtLatLong(unused_name, pt, mapsApiZoom) {
  panoramioZoom = 17 - mapsApiZoom;
  window.location = '/map/#lt=' + pt.lat() +
      '&ln=' + pt.lng() +
      '&z=' + panoramioZoom + '&k=2';
}


/**
 * Opens a fresh /map page at the desired location.
 *
 * However this function is overriden multiple times in our codebase so
 * it's hard to say what calling 'centerMapAtLatLong' will do.
 *
 * @param {string} unused_name Ignored.
 * @param {GLatLng} pt Position to center the map on.
 * @param {number} mapsApiZoom Zoom level to use, in the Maps API scale.
 */
function centerMapAtLatLong(unused_name, pt, mapsApiZoom) {
  openMapPageAtLatLong(unused_name, pt, mapsApiZoom);
}
