(function ($) {


// Create a detached dom node from an html string

$.from_html = function (html) {
  var div = document.createElement('div');
  div.innerHTML = html;
  var elem = div.childNodes[0];
  div.removeChild(elem);
  return elem;
};


// Get all attributes of an element as a hash.
// The returned hash is suitable input for the $(...).attr.(...) setter.

$.fn.attr_all = function () {
  var attr = {};

  for (var i=0; i<this[0].attributes.length; i++) {
    var node = this[0].attributes[i];
    var key = node.nodeName;
    if (key == 'class') key = 'className';
    attr[key] = node.value;
  }

  return attr;
};


// Parse a query string into a flat hash structure.
// If the same key is encountered multiple times, the value
// under that key will be an array.

$.parse_query = function (str) {

  var i = str.indexOf('?');
  if (i >= 0) str = str.substr(i+1);

  var pairs = str.split('&');
  var param = {};

  for (var i in pairs) {
    var kv = pairs[i].split('=');
    var k = decodeURIComponent(kv[0]);
    var v = decodeURIComponent(kv[1]);
    k = k.replace(/\+/g, ' ');
    v = v.replace(/\+/g, ' ');

    if (param[k] == null) {
      param[k] = v;
    }
    else {
      if (typeof(param[k]) == 'string')
        param[k] = [ param[k] ];
      param[k].push(v);
    }
  }

  return param;
};


/*
  Turn a nested structure into a flat hash suitable for
  constructing a query string. Example:
 
   { foo: { bar: 42, baz: [ 5, 6, 7 ] } }
 
  becomes
 
   {
     "foo.bar" : 42,
     "foo.baz.0" : 5,
     "foo.baz.1" : 6,
     "foo.baz.2" : 7
   }
*/

$.flatten_param = function (param) {
  var newparam = {};

  for (var k1 in param) {
    var v1 = param[k1];

    if (v1 != null && typeof v1 == 'object') {

      var subparam = $.flatten_param(v1);
      for (var k2 in subparam) {
        var v2 = subparam[k2];
        newparam[k1+'.'+k2] = v2;
      }
    }
    else {
      newparam[k1] = v1;
    }
  }

  return newparam;
};


/*
  Turn a flat hash of path specifications into a nested structure.
  This is the inverse operation of $.flatten_param. For example,
  the input:
 
   {
     "foo.bar" : 42,
     "foo.baz.0" : 5,
     "foo.baz.1" : 6,
     "foo.baz.2" : 7
   }

  becomes

   { foo: { bar: 42, baz: [ 5, 6, 7 ] } }

*/

$.unflatten_param = function (param) {

  var newparam = { root: null };

  // For each pathspec, insert a value.
  
  $.each( param, function(pathspec,val) {

    var path = pathspec.split( "." );
    path.unshift( "root" );

    // Descend into the structure via the path.

    var p = newparam;

    for (var i=0; i<path.length-1; i++) {

      var key1 = path[i];
      var key2 = path[i+1];
      var want_array = key2.match(/^\d+$/);

      if (p[key1] == null) {

        // Nothing currently occupying this slot,
        // so create an array or hash there.

        if (want_array)
          p[key1] = [];
        else
          p[key1] = {};
      }
      else if (isArray(p[key1]) && !want_array) {

        // Slot already occupied by an array, but we have a non-numeric key.
        // Upgrade the array to a hash.

        var h = {};

        for (var j=0; j<p[key1].length; j++)
          h[j] = p[key1][j];

        p[key1] = h;
      }
      else if (typeof p[key1] != 'object') {
      
        // Slot already occupied by eg. a number or string,
        // preventing us from indexing in.

        var path_so_far = path.slice(1, i+1).join(".");
        throw( "unflatten: nested data conflict at path " + path_so_far );
      }

      // If we're at the end of the path, insert the value.
      // Otherwise, descend deeper into the structure.

      if (i == path.length-2) {

        if (p[key1][key2] != null)
          throw( "unflatten: nested data conflict at path " + pathspec );
        else
          p[key1][key2] = val;

      }
      else
        p = p[key1];
    }

  } );

  return newparam.root ? newparam.root : {};
};


// Takes a date object and returns a DB suitable date string

$.format_date = function (dateObj) {
    var monthStr = dateObj.getMonth() + 1;
    if( monthStr < 10 )
	monthStr = '0' + '' + monthStr;

    var dayStr = dateObj.getDate();
    if( dayStr < 10 )
	dayStr = '0' + '' + dayStr;

    return dateObj.getFullYear() + '-' 
	 + monthStr + '-'
         + dayStr
         ;
}; // format_date

// Parse a date string "mm/dd/YYYY" and return a Date object
// Will return null if parsing fails

$.parse_date = function (dateStr) {

    // Validate date
    if( typeof dateStr === "undefined" || !dateStr ) {
        return null;
    }
    
    var year, month, day;
    var hour, min, sec;
    if( /^\d{1,2}\/\d{1,2}\/\d{4}$/.exec( dateStr ) ) {

        // Parse date
        var dateParts = dateStr.split( '/' );
        for( var i = 0; i < dateParts.length; i++ ) {
	    dateParts[i] = dateParts[i].replace( /^0/, "" );
	    dateParts[i] = parseInt( dateParts[i] );
        }

        // And do some more validation
        if( dateParts[0] < 1 || dateParts[0] > 12           // validate month
	    || dateParts[1] < 0 || dateParts[1] > 31        // validate day
	    || dateParts[2] < 1900 || dateParts[2] > 3000 ) // validate year
        {
	    return null;
        }

        year = dateParts[2];
        month = dateParts[0];
        day = dateParts[1];
    }
    else if( (match = /^(\d{4})-(\d{1,2})-(\d{1,2}) +(\d{1,2}):(\d{1,2}):(\d{1,2})(.[0-9+-]+)?$/.exec( dateStr )) )
    {
        year = match[1];
        month = match[2];
        day = match[3];
        hour = match[4];
        min = match[5];
        sec = match[6];
    }

    if( year == null )
        return null;

    // Finally, enough validate, create our date object
    var date = new Date();
    date.setFullYear( year );
    date.setMonth( month - 1 );
    date.setDate( day );

    if( hour != null ) {
        date.setHours( hour );
        date.setMinutes( min );
        date.setSeconds( sec );
    }
    
    return date;
};

$.delta_days = function (date1, date2) {

    // Copy the dates
    var date1Norm = new Date( date1.getTime() );
    var date2Norm = new Date( date2.getTime() );
    
    // Set the times equal
    date1Norm.setHours( 0 );
    date1Norm.setMinutes( 0 );
    date1Norm.setSeconds( 0 );
    date1Norm.setMilliseconds( 0 );

    date2Norm.setHours( 0 );
    date2Norm.setMinutes( 0 );
    date2Norm.setSeconds( 0 );
    date2Norm.setMilliseconds( 0 );

    // Take the epoch which should only differ in multiples of a day
    var epoch1 = date1Norm.getTime();
    var epoch2 = date2Norm.getTime();

    var oneDay = 1000 * 60 * 60 * 24; // milli * secs * mins * hours

    var deltaSeconds = epoch2 - epoch1;
    
    return deltaSeconds / oneDay;
}; // delta_days

$.format_datetime = function ( dateObj, fmt, opts ) {
    if( typeof dateObj == "undefined" || dateObj == null )
        return null;

    if( typeof opts == "undefined" || opts ==  null ) 
        opts = {};

    if( fmt == 'friendly' ) {

	// get today
        var now = new Date();

        var dd = $.delta_days( dateObj, now );

	var datepart = null;
	if( dd == 0 ) // then Today
	{
	    var timefmt = opts['short'] ? '%I:%M%p': '%I%p';
	    datepart = $.format_datetime( dateObj, timefmt );
	    datepart = datepart.replace( /^0+/g, '' ) //eliminate leading zeros
            if( opts['short'] )
	        datepart = datepart.toLowerCase();
            
            if( !opts['short'] )
	        datepart = "Today " + datepart;
	}
	else if( !opts['short'] && dd == 1 ) // yesterday
	{
	    datepart = 'Yesterday';    
	}
	else if( !opts['short'] && dd == -1 ) // tmw
	{
	    datepart = 'Tomorrow';
	}
	else if( opts['short'] && dd <= 365) // <= year
	{
	    datepart = $.format_datetime( dateObj, '%b %e' );
	}
	else // > year
	{
	    if( dateObj.getFullYear() == now.getFullYear() ) // this year
	    {
		datepart = $.format_datetime( dateObj, '%b %e' );
	    }
	    else
	    {
		datepart = $.format_datetime( dateObj, '%b %e, %Y' );
	    }
	}

        return datepart;
    }
    else if( /(%[IMpbeY])/.test( fmt ) )
    {
        var abbrMonths = [ "Jan.", "Feb.", "Mar.", "Apr.", "May", "Jun.", 
                           "Jul.", "Aug.", "Sep.", "Oct.", "Nov.", "Dec." ];

        var hour = dateObj.getHours();
        var hour12 = hour == 0 ? 12
                   : hour > 12 ? hour - 12
                   :             hour
                   ;
        
        var minute = dateObj.getMinutes();
        
        var amPM = hour >= 12 ? 'PM' : 'AM';

        var month = dateObj.getMonth();

        var day = dateObj.getDate();
        var trimmedDay = (day + "").replace( /^0/, '' );
        
        var fullYear = dateObj.getFullYear();

        var dateStr = fmt;
        
        dateStr = dateStr.replace( /%I/g, hour12 );
        dateStr = dateStr.replace( /%M/g, minute );
        dateStr = dateStr.replace( /%p/g, amPM );
        dateStr = dateStr.replace( /%b/g, abbrMonths[month] );
        dateStr = dateStr.replace( /%e/g, trimmedDay );
        dateStr = dateStr.replace( /%Y/g, fullYear );
        
        return dateStr;
    }

    return null;
}; // format_datetime

$.parse_number = function ( number ) {
    var match_number = /^[\n\f\r\s\t]*([0-9, ]+)[\n\f\r\s\t]*$/;
    var matches      = match_number.exec( number );
    
    var parsed_number = null;

    if( matches ) {
	parsed_number = matches[0].replace( ',', '' ).replace( ' ', '' );
    }

    if( parsed_number != null )
        parsed_number = parseInt( parsed_number, 10 );
    
    return parsed_number;
}; // parse_number

$.parse_money = function ( money ) {
    var match_money  = /^[\n\f\r\s\t]*\$?[\n\f\r\s\t]*([0-9, ]+(\.\d{1,2}|\.)?)[\n\f\r\s\t]*$/;
    var matches      = match_money.exec( money );
    
    var parsed_money = null;

    if( matches ) {
	parsed_money = matches[1].replace( /[ ,]/g, '' );
    }
    
    if( parsed_money != null )
        parsed_money = parseFloat( parsed_money );

    return parsed_money;
} // parse_money

$.format_money = function ( money, trimZeroCents ) {
    var formatted_money = money;

    var missing_zero = /^-?\d+\.\d$/;
    if( missing_zero.exec( money ) )
    {
	formatted_money += '0';
    }
    else if( (new String( formatted_money )).indexOf( '.' ) == -1 )
    {
	formatted_money += '.00';
    }

    formatted_money = '$' + $.commify( formatted_money );

    formatted_money = formatted_money.replace( /\.00$/, '' );

    return formatted_money;
} // format_money

$.commify = function ( number ) {
    var commified_number = number;

    commified_number = $.reverse_string( commified_number );
    commified_number = commified_number.replace( /(\d{3})(?=\d)(?!\d*\.)/g, "$1," );
    commified_number = $.reverse_string( commified_number );
    
    return commified_number;
} // commify

$.reverse_string = function (str) {
    str = new String( str );
    var reversed_string = null;

    if( str != null )
    {
	reversed_string = "";
	
	if( str.length > 0 ) {
	    for( var i = str.length-1; i >= 0; i-- ) {
		reversed_string += str.charAt( i );
	    }
	}
    }

    return reversed_string;
} // reverse_string

$.max = function () {
  var max = null;

  $( arguments ).each( function() {
    if (max == null || this > max)
      max = this;
  } );

  return max;
};

$.min = function() {
  var min = null;

  $( arguments ).each( function() {
    if (min == null || this < min)
      min = this;
  } );

  return min;
};

$.trim = function ( str ) {
    var trimmed = str.replace( /^\s+/, '' );
    trimmed     = str.replace( /\s+$/, '' );

    return trimmed;
};

$.ucfirst = function( str ) {
  return str.substr(0,1).toUpperCase() + str.substr(1);
};

$.lcfirst = function( str ) {
  return str.substr(0,1).toLowerCase() + str.substr(1);
};


$.rubyConfirm = function ( opts ) {

  var defaults = {
    title: "Confirm",
    msg: "Are you sure you want to proceed?",
    onconfirm: function() {},
    onreject: function() {}
  };

  var params = $.extend( true, defaults, opts );

  var dlg = $( $.from_html('<div></div>') ).dialog({
    modal: true,
    title: params.title,
    autoOpen: false,
    buttons: {
      Yes: function() {
        $(this).dialog('close');
        $(this).data('onconfirm').call();
      },
      No: function() {
        $(this).dialog('close');
        $(this).data('onreject').call();
      }
    }
  });

  dlg.data( 'onconfirm', params.onconfirm );
  dlg.data( 'onreject', params.onreject );
  dlg.html( params.msg );
  dlg.dialog("open");

}; // rubyConfirm


$.rubyFormConfirm = function ( opts ) {
    opts.onconfirm = function () {
	var act = $('#action');
	act.val( opts.action );
	act.parents( 'form' )[0].submit();
    };
    
    $.rubyConfirm( opts );
}; // rubyConfirmCancel


$.rubyError = function ( title, message, onClose ) {

  if( typeof title === "undefined" || title === null )
      title = 'Ruby System Error';
    
  if( typeof message === "undefined" || message === null )
      message = 'A systems error has occurred.  '
              + 'Please try again in a few minutes'
              ;


  var params = {
      modal: true,
      title: title,
      autoOpen: false,
      buttons: {
          Ok: function() {
              $(this).dialog('close');
          }
      }
  };

  if( onClose )
      params.close = onClose;
     
  var dlg = $( $.from_html('<div></div>') ).dialog(params);

  dlg.html(message);
  dlg.dialog("open");

}; // rubyError


// Ajax request error handler

$.rubyAjaxError = function (req, textStatus, errorThrown) {
  if (req.status == 403)   // Session expired
    window.location.reload();
  else
    $.rubyError(
      "AJAX Error",
      "An error occurred while making an ajax request: " +
        "(" + req.status + ") " + req.statusText
    );
}; // rubyAjaxError

// Enable or disable a jQuery ui widget

$.fn.enableWidget = function ( flag ) {

    if (flag) {
      $( this ).removeClass( 'ui-state-disabled' );
      $( this ).removeAttr( 'disabled' );
    }
    else {
      $( this ).addClass( 'ui-state-disabled' );
      $( this ).attr( 'disabled', 'disabled' );
    }

}; // $.fn.enableWidget

$.selectmenuSwap = function () {
    // Loop through each select option with the rtg-swap class
    var select = this;
    var swaps  = $( '.rtg-swap', select );
    if( swaps.length > 0 ) {
	swaps.each( function () {

	    // Parse out the id of the HTML element to toggle visibility
	    var option    = this;
	    var swapClass = $.grep( 
		option.className.split( / / ),
		function (n) {
		    return n.indexOf( 'rtg-swap-' ) == 0;
	    });
	    var swapId = swapClass[0].substr( "rtg-swap-".length );

	    // Show or hide that element based on if this option is selected
	    if( option.selected ) {
		$( '#' + swapId ).show();
	    }
	    else {
		$( '#' + swapId ).hide();
	    }
	} );
    }

    return;
}; // selectmenuSwap

$.parseDataFromElementClass = function (elm, strOrRegex) {
    var data = null;

    var isRegex = typeof strOrRegex == 'object' || typeof strOrRegex == 'function';

    var classes = $(elm).attr( 'class' ).split( /\s+/ );
    for( var i = 0; i < classes.length; i++ ) {
        var classStr = classes[i];
        
        if( isRegex ) {
            var matches = strOrRegex.exec( classStr );
            
            if( matches && matches[1] ) {
                data = matches[1];
                break;
            }
        }
        else if( classStr.indexOf( strOrRegex ) == 0 ) {
            data = classStr.substr( strOrRegex.length );
            break;
        }
    }

    return data;
}; // parseClass

$.rubyContentApprovalComment = function (elm) {

    elm.cluetip({
        onShow: function (ct, inner) {
            // cluetip does not provide a way to update the title when using
            // ajax; this code will use the H1 in the HTML returned as the title
            var h1 = inner.find( 'h1' ).remove();
            var title = ct.find( '#cluetip-title' );
            title.html( h1.html() );
        }, // onshow
        cluetipClass: 'jtip ruby-comment-popup',
        attribute: 'rel',
        dropShadow: true,
        arrows: true,
        clickThrough: false,
        apositionBy: 'auto',
        apositionBy: 'mouse',
        positionBy: 'bottomTop',
        sticky: false
    });
 
}; // rubyContentApprovalComment

$.isDefined = function (variable) {
    return typeof variable !== "undefined" && variable != null;
}

$.supportsVideo = function(){
    //* checks if <video> tag is supported
  return !!document.createElement('video').canPlayType;
}

// adding a function to add the * for msie browsers
//rtg-layout .rtg-widget.required .rtg-label label
$.addAsterisks = function(selector){
    if( ($.browser.msie && $.browser.version == "6.0" || $.browser.version == "7.0")){
        $(selector).each(function(i,e){
           $(e).html($(e).html()+"*");
        });
    }
}

// profileCode( label, code )
//
//     profileCode( 'a.sitemap', function () {
//         var links = $( 'a.sitemap' );
//         links.initSometing();
//         return links.length;
//     });
//
// Time how long it takes a block of code to run
// Log out that time with the suplied label
// If the block of code returns a number, the output that number in
// the logs, also
//
$.profileCode = function( label, code )
{
    var t1 = (new Date()).getTime();
    var count = code();
    var t2 = (new Date()).getTime();
    if( typeof count != 'undefined' && count != null )
        console.log( label + ': ' + count );
    else
        console.log( label );
    console.log( '  ^ ' + (t2-t1)/1000 + ' seconds' );
}

})(jQuery); // function ($)


