/**
 * RunTrackr - Browse/Find Routes Scripts
 */

// TODO: Refactor the back-button/history/state preservation code into a
// utility method/library. Specifically the code dealing with the fragment/
// and expectedFragment.
// DESIGN:
// 1) Action that causes state change and should be preserved:
// - Save state.
// - Generate URI fragment and push onto browser stack. (History)
// - Do not interpret this as a history/state change since it has already been
// done. (Opposite - state change triggers URI change)
// - Accomplish by ???
// 2) Restore history/state
// - Detect the URI change.
// - Recreate state from the URI fragment obtained.
// - (Some of the same steps from #1 also need to be done)
// - Opposite: URI change triggers state change)
// - Accomplish by having a onHistoryChange callback to handle this.
//   - Eg: The `checkUriFragmentChanged` takes a callback function that will
//     be called with the old and new fragments so specific actions can be
//     taken.
// TODO: NOTE: This currently doesn't support IE; use hidden iframe for this?
// What about Opera?
// 3) May want to just look at Really Simple History (RSH) and use that.
// - Has to be on a LIVE server to work, not read from a file.

/**
 * Holds the {@link runtrackr.ListingMap} reference.
 */
var listingMap;

/**
 * Stores the expected hash (URI fragment) so that changes can be detected
 * and appropriate actions taken.  This is used to preserve back-button
 * functionality.
 */
var expectedFragment = '';

/**
 * Polling interval to check the URI for changes, usually due to back-forward
 * browser actions. (In ms)
 */
var pollingInterval = 100;

// Reduces memory leaks from circular references.
jQuery(document).unload(function()
{
  GUnload();
});

// Run when the DOM is ready.
jQuery(document).ready(function()
{
  // Check if browser supports GMaps.
  if (GBrowserIsCompatible())
  {
    // Add a tooltip to the route/location search bar and focus the field.
    runtrackr.Utility.addTooltip('#location-input');
    jQuery('#location-input').focus();

    // Initialize the map.
    listingMap = new runtrackr.ListingMap(
    {
      mapId : 'map',
      ajaxLoadRoutesUrl: jQuery('form.location-jump-to').attr('action'),
      viewRouteUrl: jQuery('#view-url').val(),
      mapsApiKey : jQuery('#maps-api-key').val()
    });

    // Continuously check if the URI fragment has changed so map state can
    // be updated. (There is no event handler for this)
    enableUriHistoryPolling(pollingInterval);

    // Event handler to process searches for routes.
    jQuery('form.location-jump-to').submit(submitSearchHandler);

    // Hide the suggestion box when search field focused.
    jQuery('#location-input').focus(runtrackr.Utility.hideLocationMatches);
  }
});

/**
 * Event handler to process searches for routes.
 */
function submitSearchHandler(e)
{
  e.preventDefault();

  // Necessary to blur() the input field if submitted via 'Enter'.
  jQuery('#location-input').blur();

  var locationValue = jQuery('#location-input').val();

  // Lookup routes and pass in callback functions.
  listingMap.findRoutes(
    locationValue,
    {
      success: findRoutesSuccess,
      failure: findRoutesFailure
    }
  );
}

/**
 * Periodically checks the URI to see if the fragment/hash has been changed
 * by a user action.
 *
 * @param Number interval the number of milliseconds between each poll cycle.
 */
function enableUriHistoryPolling(interval)
{
  // TODO: This could be improved/tweaked for performance, etc.
  // EG: Should we store the interval ID so it can be deregistered with
  // `clearInterval()` later on? (Eg on body unload)

  // Must call at the beginning once since the user could have navigated to
  // this resource with the fragment already set.
  checkUriFragmentChanged();
  window.setInterval('checkUriFragmentChanged()', interval);
}

function checkUriFragmentChanged()
{
  // TODO: Use a callback function to change the expectedFragment or
  // cause some action to occur...
  // Then can pass in the expected hash as a value.

  // Check if the fragment has changed and that it is not empty.
  if (window.location.hash != expectedFragment
    && window.location.hash.length > 1)
  {
    expectedFragment = window.location.hash;

    // Must extract location information without the '#' symbol.
    var fragment = expectedFragment.substr(1);
    var locationValue = decodeURIComponent(fragment);

    // Set the new location in the search bar to indicate the change.
    jQuery('#location-input').val(locationValue).blur();

    // Lookup routes and pass in callbacks.
    listingMap.findRoutes(
      locationValue,
      {
        success: findRoutesSuccess,
        failure: findRoutesFailure
      }
    );
  }
}

/**
 * Callback function triggered when routes are found for a supplied location.
 *
 * @param Array routes a list of the returned routes.
 */
function findRoutesSuccess(routes)
{
  // Hide instructions.
  jQuery('p.instructions').hide();

  // Clear current results. (But leave template)
  var routesList = jQuery('ul.routes');
  routesList.hide();
  routesList.find("li[class!='template']").remove();


  // Location value entered by user.
  var locationValue = jQuery('#location-input').val();
  var locationHash = encodeURIComponent(locationValue);


  // TODO: Testing location back-button history preservation.
  // - Test across multiple browsers whether need to prefix with '#' or not...
  window.location.hash = '#' + locationHash;
  // Must set the expectedFragment so that the polling doesn't interpret the
  // change as one that requires a state change. (Since that's already done)
  expectedFragment = window.location.hash;
  document.title = 'Routes near ' + locationValue;




  // No routes were found.
  if (0 == routes.length)
  {
    // Update link to add a route for this location.
    var addRouteLink = jQuery('#add-url').val() + '#' + locationHash;
    jQuery('.add-route a').attr('href', addRouteLink);
    jQuery('.add-route').show();

    jQuery('.no-routes-found a').attr('href', addRouteLink);

    jQuery('.no-routes-found strong').html(locationValue);
    jQuery('.no-routes-found').show();
    jQuery('.num-routes').empty().hide();

    // No need to go any further; no results need to be displayed.
    return;
  }

  // Set display messages.
  var searchTitleValue = 'Routes near ' + locationValue;
  jQuery('#route-info h2.title').empty().append(searchTitleValue);

  // Hide the 'no routes found' message.
  jQuery('.no-routes-found').hide();

  // Update link to add a route for this location.
  var addRouteLink = jQuery('#add-url').val() + '#' + locationHash;
  jQuery('.add-route a').attr('href', addRouteLink);
  jQuery('.add-route').show();

  // Go through each route in the list and add it the sidebar.
  for (var i in routes)
  {
    var route = routes[i];

    // Convert distance to numeric value.
    var distance = parseFloat(route.distance);
    if (isNaN(distance))
    {
      distance = 0;
    }

    // Add the result to the side bar, using the template for results.
    var routeSidebarEntry = jQuery('.routes .template').clone();
    routeSidebarEntry.removeClass('template');
    routeSidebarEntry.find('.name').text(route.name).attr('id', 'route-id-' + route.id);
    routeSidebarEntry.find('.distance .value').text(distance.toFixed(2));
    routeSidebarEntry.find('.description').text(route.description);

    // TODO: This is a quick fix to get miles to display as well... remove once
    // have user-specific setting. (Make miles default)
    routeSidebarEntry.find('.distance .miles.value').text(
      (distance/runtrackr.DISTANCE_UNITS.CONVERSIONS.MILES_TO_KMS).toFixed(2)
    );

    routesList.append(routeSidebarEntry);

    // Event handler to cause the marker's infoWindow to popup when route
    // listing is clicked and to highlight result when clicked.
    jQuery('a#route-id-' + route.id).click(routeListingClickCallback).click(highlightSelectedResult);

  }

  jQuery('.num-routes').html(routes.length + ' routes found').show();

  // Nice slide-down animation for search results.
  // NOTE: Cannot animate to height 'auto'; thus must first grab the actual
  // height in pixels, then set to '0', THEN animate to this height AND THEN
  // restore 'auto' on height so that height is no longer fixed.
  var actualHeight = jQuery('ul.routes').height();
  routesList.css({height: '0'});
  routesList.animate({height: actualHeight}, 'slow', 'swing', function(){jQuery(this).css({height: 'auto'});});
}

/**
 * Highlights the selected result when clicked.
 */
function highlightSelectedResult(e)
{
  e.preventDefault();

  // Remove previous highlighting and highlight the current result.
  jQuery('ul.routes li').removeClass('selected');
  jQuery(this).parent().parent().addClass('selected');
}

/** * Callback handler to allow route listings to be clicked and show up on the
 * map.
 *
 * @param Event e the triggering event.
 */
function routeListingClickCallback(e)
{
  e.preventDefault();

  var routeId = jQuery(this).attr('id');

  listingMap.getMarkers()[routeId].popupInfoWindow();
}

/**
 * Callback function triggered when the location supplied could not be
 * resolved with the geocoder.
 *
 * @param String errorCode the error code returned.
 * @param Object data associated data returned by the caller.
 */
function findRoutesFailure(errorCode, data)
{
  var errorMessage = '';

  if (errorCode == runtrackr.Map.ERROR.LOCATION_NOT_FOUND)
    errorMessage =  'Could not locate the address.';
  else if (errorCode == runtrackr.Map.ERROR.LOCATION_NOT_ACCURATE)
    errorMessage = 'Please be more specific.';
  else if (errorCode == runtrackr.Map.ERROR.LOCATION_MULTIPLE_MATCHES)
  {
    // Show the list of multiple matches/suggestions.
    runtrackr.Utility.showLocationMatches(data, setLocationFromMultipleCallback);

    // Stop further execution.
    return;
  }
  else
    errorMessage = 'Something bad happened.';

  window.alert(errorMessage);

  jQuery('#location-input').focus();
}

/**
 * Sets the location state of the map after user clicks on a location from the
 * suggestion list of multiple matches.
 */
function setLocationFromMultipleCallback(placemark)
{
  // Update the location in the search bar.
  jQuery('#location-input').val(placemark['address']);

  // Lookup routes and pass in callback functions.
  listingMap.findRoutes(
    placemark['address'],
    {
      success: findRoutesSuccess,
      failure: findRoutesFailure
    }
  );
}
