//
// load an area of a page with ajax
//
// show pretty spinners during loading
// do nice fadin/fadeout transition animations
// gracefully handles aborting previous load when the same area is changed before the previous one finished loading
// handles displaying simple error when ajax call fails
// optionally can handle caching multiple content areas in separate divs, and just showing/hiding them when possible
//
// requires jquery
//

// settings

// millisecond animation length times
window.anim_times = {
  wait_in:   10, // 'waiting' spinner fading in
  wait_out:  10, // 'waiting' spinner fading out
  fade_in:  170, // body area fading in
  fade_out: 200  // body area fading out
}
// 'waiting' spinner html to use, when ajax is loading
window.spinner_html = '<div class="spinner-container"><div class="spinner"></div></div>';
// error html to use, when ajax fails
window.error_html = '<div class="error"><div class="error-title">Error Retrieving Content</div><div class="error-body">Check your connection or try again later.</div></div>';

// public functions

// fade a body area out and load a new one (no ajax, just loads in new content given)
function crossFade(queue, body, data) {
  if (! body || ! data || innerHTMLEqual(body.innerHTML, data)) return;
  // abort previous queue
  jQuery(queue).clearQueue();
  // abort any playing animations
  stopBodyAnims(queue, body);
  // fade old body area out
  if (! innerHTMLEqual(body.innerHTML, ''))
    fadeOut(queue, body, window.anim_times.fade_out);
  // fade in new body
  setHTML(queue, body, data);
  setVisibility(queue, body, 'visible');
  fadeIn(queue, body, window.anim_times.fade_in);
}

// fade a body area out and load a new one in via ajax with spinner (no caching of any other bodies, always reloads via ajax)
function crossFadeBody(queue, body, url, beforecallback, donecallback) {
  if (! body || ! url) return;
  // abort previous queue
  jQuery(queue).clearQueue();
  // abort any previous ajax calls
  stopAjax(queue);
  // abort any playing animations
  stopBodyAnims(queue, body);
  // run initial callback
  doCallback(queue, body, beforecallback);
  // fade old body area out
  if (! innerHTMLEqual(body.innerHTML, ''))
    fadeOut(queue, body, window.anim_times.fade_out);
  // load new body area in via ajax
  loadAjax(queue, body, url, donecallback);
}

// fade out and fade in a cached body area, showing spinner and ajax loading if it hasn't been loaded yet
// you can pass it a separate after-ajax-version-shown callback, and after-cached-version-shown callback
// note: if any areas come pre-loaded (at page load time, or via nested ajax) you must set body.finishedinit = true for them
// so it knows they are properly loaded (this mechanism is used to detect if it was aborted and discard possibly borked cache and retry next time)
function crossFadeCachedBody(queue, allbodies, oldbody, body, url, beforecallback, loadedcallback, selectedcallback) {
  if (! queue || ! allbodies || ! body || ! url) return;
  // abort previous queue
  jQuery(queue).clearQueue();
  // abort any previous ajax calls
  stopAjax(queue);
  // abort any playing animations
  stopAnims(queue, allbodies);
  // run initial callback
  doCallback(queue, body, beforecallback);
  // fade out old one
  if (oldbody)
    fadeOut(queue, oldbody, window.anim_times.fade_out);
  // hide any aborted ones
  jQuery(queue).queue(function(){
    allbodies.hide();
    jQuery(this).dequeue();
  });
  // if it hasn't been loaded yet or was previously interrupted, load via ajax
  if (innerHTMLEqual(body.innerHTML, '') || innerHTMLEqual(body.innerHTML, window.spinner_html) || innerHTMLEqual(body.innerHTML, window.error_html) || ! body.finishedinit) {
    loadAjax(queue, body, url, loadedcallback);
  // otherwise if it has been loaded before, just show it
  } else {
    fadeIn(queue, body, window.anim_times.fade_in);
    doCallback(queue, body, selectedcallback);
  }
}

// internal functions

function loadAjax(queue, body, url, donecallback) {
  // show the waiting spinner
  setHTML(queue, body, window.spinner_html);
  fadeIn(queue, body, window.anim_times.wait_in);
  // invoke ajax call (remembers object so we can abort it)
  jQuery(queue).queue(function(){
    this.ajaxobj = jQuery.ajax({
      url: url,
      dataType: 'html',
      success: function(data, msg, hxr){
        // switch from waiting spinner to real content
        fadeOut(queue, body, window.anim_times.wait_out);
        setHTML(queue, body, data);
        fadeIn(queue, body, window.anim_times.fade_in);
        // run callback if there is one
        doCallback(queue, body, donecallback);
        // remember now that we're done
        jQuery(queue).queue(function(){
          body.finishedinit = true;
          jQuery(this).dequeue();
        });
      },
      error: function(xhr, msg, err){
        // error handling
        setHTML(queue, body, window.error_html);
      },
      complete: function(xhr, msg) {
        // once it's complete (success or error), no need to remember for aborting purposes anymore
        queue.ajaxobj = null;
      }
    });
    jQuery(this).dequeue();
  });
}
// shortcuts for common queued actions
function fadeOut(queue, body, time) {
  jQuery(queue).queue(function(){
    jQuery(body).fadeOut(time, function(){
      jQuery(this).css('opacity', null);
      if (queue != body) jQuery(queue).dequeue();
    });
    if (queue == body) jQuery(queue).dequeue();
  });
}
function fadeIn(queue, body, time) {
  jQuery(queue).queue(function(){
    jQuery(body).fadeIn(time, function(){
      jQuery(this).css('opacity', null);
      if (queue != body) jQuery(queue).dequeue();
    });
    if (queue == body) jQuery(queue).dequeue();
  });
}
function setVisibility(queue, body, how) {
  jQuery(queue).queue(function(){
    body.style.visibility = how;
    jQuery(this).dequeue();
  });
}
function setHTML(queue, body, data) {
  jQuery(queue).queue(function(){
    body.innerHTML = data;
    jQuery(this).dequeue();
  });
}
function doCallback(queue, body, callback) {
  jQuery(queue).queue(function(){
    body.callback = callback;
    if (body.callback) body.callback();
    jQuery(this).dequeue();
  });
}

// shortcuts for some queue handling
function stopBodyAnims(queue, body) {
  if (queue == body)
    jQuery(body).stop(true, true);
  else
    stopAnims(queue, jQuery(body));
}
function stopAnims(queue, where) {
  jQuery(queue).queue(function(){
    where.clearQueue();
    where.stop(true, true);
    jQuery(this).dequeue();
  });
}
function stopAjax(queue) {
  jQuery(queue).queue(function(){
    if (this.ajaxobj) this.ajaxobj.abort();
    jQuery(this).dequeue();
  });
}


// Browsers don't return elem.innerHTML the same way you set it... this tries to compare them in certain cases anyway...
// IE changes case, white space, and quote marks (and a lot of other things we don't use (hopefully))
// FF changes XHTML <xxx /> tags to <xxx>
function innerHTMLEqual(str1, str2) {
  return _mungeHTML(str1) == _mungeHTML(str2);
}
function _mungeHTML(str) {
  return str.toLowerCase().replace(/['" \r\n\t]/g, '').replace(/<([^>]*?) ?\/?>/g, '<img$1>');
}

