// This file is for little stuff that doesn't fit in elsewhere and depends only on prototype.js.

// Returns a mixin that implements a named callback.
//
// Example use:
//
//   var MyClass = Class.create(hasCallback('onChange'), {
//     ...
//   });
//
// MyClass now has a callback property called onChange, a boolean property called _suspendOnChange (false by default), and a function called _fireOnChange.
function hasCallback(name) {
  var nameSuffix = name.charAt(0).toUpperCase() + name.slice(1);
  var suspendName = '_suspend'+nameSuffix;
  var fireName = '_fire'+nameSuffix;
  var result = {};
  
  result[name] = null;
  result[suspendName] = false;
  result[fireName] = function() {
    if(this[name] && !this[suspendName]) {
      this[name].apply(null, arguments);
    };
  };
  
  return result;
}

// Creates a new element under parent with the specified properties, any of which may be left blank.
function newElt(type, id, klass, parent) {
	var elt = new Element(type || 'span');
  if(id) elt.writeAttribute('id', id);
  if(klass) elt.writeAttribute('class', klass);
  if(parent) parent.insert({ bottom: elt });
  return elt;
}

function setupTracker(elm)
{
  elm.observe('click', trackEvent.curry(elm.readAttribute('trackable').split('/')));
}

function trackEvent(category, action, label) {
  try {
    if (label === undefined) {
      pageTracker._trackEvent(category, action); 
    } 
    else {
      pageTracker._trackEvent(category, action, label); 
    }
  } 
  catch (e) {} ;
}

function formatPrice(price) {
  return "$" + (price/100).ceil().toFixed(0);
}

function formatFromPrice(price) {
  return "from " + formatPrice(price);
}

function splitHoursFromMinutes(minutes) {
  var hours = (minutes / 60).floor();
  var remMinutes = ((minutes / 1).floor() % 60);
  return { hours: hours, minutes: remMinutes };
}

function formatDuration(minutes, padMinutes) {
  var dur = splitHoursFromMinutes(minutes);
  var minuteStr = padMinutes ? dur.minutes.toPaddedString(2)
                             : dur.minutes.toString();
  return dur.hours.toString() + "h " + minuteStr + "m";    
}

function formatTime12(minutes) {
  var parts = dissectTime(minutes);
  return parts.hour12.toString() + ":" + parts.minute.toPaddedString(2) +
    parts.meridiem;
}

function formatTime24(minutes) {
  var parts = dissectTime(minutes);
  return parts.hour24.toString() + ":" + parts.minute.toPaddedString(2);
}

function formatTimeShort(minutes) {
  var parts = dissectTime(minutes);
  return parts.hour12.toString() + parts.meridiem;
}

function dissectTime(minutes) {
  var dur = splitHoursFromMinutes(minutes);
  var modHours = dur.hours % 12;
  
  return {
    hour24: dur.hours,
    minute: dur.minutes,
    hour12: (modHours == 0 ? 12 : modHours),
    meridiem: (dur.hours < 12 ? 'am' : 'pm')
  };
}

// Takes time as number of minutes since midnight
var formatTime = formatTime12;

function formatTimeFromDate(date) {
  return formatTime(minutesSinceMidnight(date));
}

function formatDOWFromDate(date) {
  return dayNames[date.getDay()];
}

function formatUltraAbbrevDOWFromDate(date) {
  return ultraAbbrevDayNames[date.getDay()];
}

function formatAbbrevDOWFromDate(date) {
  return abbrevDayNames[date.getDay()];
}

function minutesSinceMidnight(date) {
  return date.getHours() * 60 + date.getMinutes();
}

var monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", "Smarch"];
var abbrevMonthNames = ["Jan", "Feb", "Mar", "Apr", "May", "June", "July", "Aug", "Sept", "Oct", "Nov", "Dec"];
var dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
var ultraAbbrevDayNames = ["Su", "M", "Tu", "W", "Th", "F", "Sa"];
var abbrevDayNames = ["Sun", "Mon", "Tues", "Wed", "Thurs", "Fri", "Sat"];

function genericFormatDate(dayNames, monthNames, dte, blnYear, blnDayOfWeek)
{
  var yearStr = blnYear ? ", " + dte.getFullYear() : "";
  var dowStr = blnDayOfWeek ? dayNames[dte.getDay()] + " " : "";
	return dowStr + monthNames[dte.getMonth()] + " " + dte.getDate() + yearStr;
}

var formatAbbrevDate =
  genericFormatDate.curry(abbrevDayNames, abbrevMonthNames);
var formatDate =
  genericFormatDate.curry(dayNames, monthNames);

function pureVirtual() {
  throw new Error("This method must be defined by subclasses");
}

function groupBy(keyExtractor, l) {
  return l.inject(new Hash(), function(acc, o) {
    var key = keyExtractor(o);
    var thisSet = acc.get(key) || [];
    thisSet.push(o);
    acc.set(key, thisSet);
    return acc;
  });
}

function getTime() {
  return (new Date).getTime();
}

function isGlobalDefined(name) {
  return !(window[name] === undefined);
}

function consoleSupports(fnName) {
  return isGlobalDefined('console') && console[fnName];
}

function logDuration(fn, taskName) {
  if(!taskName) throw new Error("logDuration must be given a task name");
  
  if(consoleSupports('time')) {
    var result;
    console.time(taskName);
    try {
      result = fn();
    } finally {
      console.timeEnd(taskName);
    }
    return result;
  } else {
    return fn();
  }
}

function groupConsoleOutput(fn, message) {
  if(consoleSupports('group')) {
    console.group(message);
    var result = fn();
    console.groupEnd();
    return result;
  } else {
    return fn();
  }
}

function pluralize(number, text)
{
	if( parseInt(number, 10) == 1 )
		return text;
	else
		return text + "s";
}

function arrayEltsEqual(a1, a2) {
  return a1.length == a2.length &&
    a1.zip(a2).all(function(pair) {
      return pair[0] == pair[1];
    });
}

// Element-by-element boolean AND of all the arguments.
//
// Example:
//   >>> arrayAnd([true, false, true], [true, true, false])
//   [true, false, false]
function arrayAnd(arrays) {
  if(arrays.pluck('length').uniq().length > 1) {
    throw new Error("Arrays have different lengths");
  }
  
  // Implemented in this style for speed.
  var innerMax = arrays.length;
  var outerMax = arrays.first().length;
  var results = new Array(outerMax);
  for(var i = 0; i < outerMax; i++) {
    results[i] = true;
    for(var j = 0; j < innerMax; j++) {
      if(!arrays[j][i]) results[i] = false;
    }
  }
  
  return results;
}

function partitionByBoolArray(l, bools) {
  if(l.length != bools.length) {
    throw new Error('Boolean array length mismatch');
  }
  
  var trueArray = [];
  var falseArray = [];
  l.each(function(o, i) {
    (bools[i] ? trueArray : falseArray).push(o);
  });
  return { passed: trueArray, failed: falseArray };
}

function friendlyName(cxr) {
	carrier = AIRLINES.get(cxr.toUpperCase());
  return( carrier != null ? carrier.short_name : cxr);
}

function friendlyCarrierLongName(cxr) {
	carrier = AIRLINES.get(cxr.toUpperCase());
  return( carrier != null ? carrier.name : cxr);
}

function friendlyEquipmentName(equipment) {
  equip = AIRCRAFT.get(equipment.toUpperCase());
  return( equip != null ? equip.name : equipment);
}

function friendlyCityName(tla) {
  city = AIRPORTS.get(tla.toUpperCase());
  if (city == null) { 
    city = METROCODES.get(tla.toUpperCase());
  }  
  response = city != null ? city.city_name : tla;    
  if ( response.include(" - All Airports") ) { response = response.gsub(/ - All Airports/, ""); }
  
  return( response );
}

// Checks to make sure that a string conforms to the MM/DD/YYYY format and that the date it represents is a valid one. Does not require day and month to be zero-padded.
function isDateString(str) {
  return !! parseDateString(str);
}

// Checks the string in the same way as isDateString. If the string is valid, a new Date object is returned for it. Otherwise, null is returned.
function parseDateString(str) {
  var basicDateFormat = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/;
  var match = str.match(basicDateFormat);
  
  if(!match) return false;
  
  // We can't just create a new Date from the string to check its validity. Javascript will do things like interpreting '2/31/2008' as March 2, 2008, which would cause us to allow invalid date strings. Instead, we'll create a new date and then check to see that all the fields match up.
  var year  = new Number(match[3]);
  var month = new Number(match[1]) - 1; // JS starts months at zero
  var day   = new Number(match[2]);
  var date = new Date(year, month, day);
  var sameNumbers = (date.getMonth()    == month &&
                     date.getFullYear() == year &&
                     date.getDate()     == day);
  return sameNumbers ? date : null;
}

// parseDateString in the other direction.
function dateToString(date) {
  return (date.getMonth() + 1) + '/' + date.getDate() + '/' + date.getFullYear();
}

function stringToDate(date) {
  var result = new Date(date);
  return result;
}

function daysAfter(days, date) {
  var result = new Date(date);
  result.setDate(date.getDate() + days); // Out of 0-31 range is OK
  return result;
}

function getRadioGroupVal( formID, groupName )
{
  return Form.getInputs(formID,'radio',groupName).find(function(radio) { return radio.checked; }).value;
}

function setRadioGroupVal( formID, groupName, radioVal )
{
  var elm = Form.getInputs(formID,'radio',groupName).find( function(radio){return radio.value==radioVal;});
  if( elm ) {
    elm.checked = true;
    elm.defaultChecked = true;
  }
}

function removeAllChildren(elt) {
  // Seems to be faster than setting innerHTML.
  while(elt.hasChildNodes()) {
    elt.removeChild(elt.firstChild);
  }
}

function constantFn(x) {
  return function() { return x; };
}

function cmpNums(x, y) { return x - y; }

// true iff the array is ordered according to the comparison function given. cmp is expected to behave in the same way as a built-in Array.sort() comparison function. The cmpNums function above is a good example.
function inOrder(cmp, reverse, array) {
  var len = array.length;
  if(array.length < 2) return true;
  
  var inverter = reverse ? -1 : 1;
  
  for(var i = 1; i < len; i++) {
    if(cmp(array[i-1], array[i]) * inverter > 0) return false;
  }
  
  return true;
}

var inNumOrder = inOrder.curry(cmpNums, false);

function buildArray(fn, len) {
  return $R(0,len-1).map(fn);
}

function replicate(len, content) {
  return buildArray(function() { return content; }, len);
}

// Mixin for giving objects unique identifier numbers.
var hasUniqueID = {
  // Returns a unique ID for this object. Always returns the same ID for a single object.
  _getUniqueID: function() {
    this._uniqueID = this._uniqueID || _nextUniqueID++;
    return this._uniqueID;
  }
};
var _nextUniqueID = 0; // do NOT modify this anywhere but _getUniqueID.

// Function composition.
function compose(f, g) {
  return function() {
    return f(g.apply(this, arguments));
  };
}

// A version of Prototype's update function that doesn't do regex scans looking for scripts.
// Should be marginally faster in Firefox and an even bigger improvement in IE.
function update_no_scan(element, content)
{
  element = $(element);
  if (content && content.toElement) content = content.toElement();
  if (Object.isElement(content)) return element.update().insert(content);
  content = Object.toHTML(content);
  var tagName = element.tagName.toUpperCase();

  if ( (Prototype.Browser.IE || Prototype.Browser.Opera) && tagName in Element._insertionTranslations.tags) 
  {
    $A(element.childNodes).each(function(node) { element.removeChild(node); });
    Element._getContentFromAnonymousElement(tagName, content)
      .each(function(node) { element.appendChild(node); });
  }
  else 
    element.innerHTML = content;

  return element;
}

function verticallyCollapse(elt) {
  if (Prototype.Browser.IE)
    elt.setStyle({display: 'none'});
  else
    elt.setStyle({overflow: 'hidden', maxHeight: '0px', position: 'fixed'});
}

function verticallyExpand(elt) {
  if (Prototype.Browser.IE)
    elt.setStyle({display: 'block'});
  else
   elt.setStyle({overflow: 'visible', maxHeight: 'none', position: 'static'});
}

function isVerticallyCollapsed(elt) {
  if (Prototype.Browser.IE)
    return elt.getStyle('display') == 'none';
  else
    return elt.getStyle('overflow') == 'hidden' && elt.getStyle('max-height') == '0px';
}

function registerNS(ns) {
 var nsParts = ns.split(".");
 var root = window;

 for(var i=0; i<nsParts.length; i++)
 {
  if(typeof root[nsParts[i]] == "undefined")
   root[nsParts[i]] = new Object();

  root = root[nsParts[i]];
 }
}

// browser-neutral function to create an xmlDoc from a string
function loadXmlDoc(xmlStr)
{
  var xmlDoc = null;
  
  if( typeof(ActiveXObject) == "undefined")
  {
    parser=new DOMParser();
    xmlDoc=parser.parseFromString(xmlStr,"text/xml");  
  }
  else
  {
    xmlDoc=new ActiveXObject("Microsoft.XMLDOM");
    xmlDoc.async="false";
    xmlDoc.loadXML(xmlStr);
  }
  
  return xmlDoc;
}

// browser-neutral function to retrieve contents of an xml text node
function getTextNodeValue(el)
{
  return (el.text || el.textContent);
}
