

var DateCalculation_weekdayNames = [ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' ];

function formatIntWithZeroes(value, digits)
{
  var string = value.toString();
  while (string.length < digits)
  {
    string = '0' + string;
  }
  return string;
}

function dateServerToday()
{
  return new Date('02/08/2012');
}

function getDateObject(dateString)
{
  var dateNumber = Date.parse(dateString);
  if (!dateNumber) return null;
  return new Date(dateNumber);
}

function formatDate(dateObject)
{
  return '' + formatIntWithZeroes(dateObject.getMonth() + 1, 2) 
         + '/' + formatIntWithZeroes(dateObject.getDate(), 2)
         + '/' + formatIntWithZeroes(dateObject.getFullYear(), 4);
}

function dateAddDays(dateObject, days)
{
  var newDateObject = new Date(dateObject);
  newDateObject.setDate(newDateObject.getDate() + days);
  return newDateObject;
}

function dateAddMonths(dateObject, months)
{
  var newDateObject = new Date(dateObject);
  newDateObject.setMonth(newDateObject.getMonth() + months);
  return newDateObject;
}

function dateDifferenceDays(fromDateObject, toDateObject)
{
  return Math.round((toDateObject.getTime() - fromDateObject.getTime()) / (24 * 60 * 60 * 1000));
}

function dateIsHoliday(dateObject, bankHolidayDates)
{
  return (dateObject.getDay() == 0) || (dateObject.getDay() == 6) || bankHolidayDates[formatDate(dateObject)];
}

function skipHolidaysForward(dateObject, bankHolidayDates)
{
  var newDateObject = new Date(dateObject);
  while (dateIsHoliday(newDateObject, bankHolidayDates))
    newDateObject = dateAddDays(newDateObject, 1);
  return newDateObject;
}

function skipHolidaysBackward(dateObject, bankHolidayDates)
{
  var newDateObject = new Date(dateObject);
  while (dateIsHoliday(newDateObject, bankHolidayDates))
    newDateObject = dateAddDays(newDateObject, -1);
  return newDateObject;
}

function skipHolidays(dateObject, holidaySkippingType, bankHolidayDates)
{
  if (!dateObject) return null;
  if (!holidaySkippingType || !bankHolidayDates) return dateObject;
  var earlierBusinessDay = skipHolidaysBackward(dateObject, bankHolidayDates);
  var laterBusinessDay = skipHolidaysForward(dateObject, bankHolidayDates);
  switch (holidaySkippingType)
  {
    case 1:
    default:
      return dateObject;
    case 2:
      return earlierBusinessDay;
    case 3:
      return laterBusinessDay;
    case 4:
      if (dateDifferenceDays(earlierBusinessDay, dateObject) <= dateDifferenceDays(dateObject, laterBusinessDay))
      {
        return earlierBusinessDay;
      }
      else
      {
        return laterBusinessDay;
      }
    case 5:
      if (dateDifferenceDays(dateObject, laterBusinessDay) <= dateDifferenceDays(earlierBusinessDay, dateObject))
      {
        return laterBusinessDay;
      }
      else
      {
        return earlierBusinessDay;
      }
  }
}

function getHolidaySegment(dateObject, bankHolidayDates)
{
  // Returns the array consisting of the begin and end of a "holiday segment" 
  // that includes the specified date. The begin date is included in the segment,
  // the end date is not. For non-holiday date, an empty segment (begin = end = date)
  // is returned.
  var segmentBegin = dateAddDays(skipHolidaysBackward(dateAddDays(dateObject, -1), bankHolidayDates), 1);
  var segmentEnd = skipHolidaysForward(dateObject, bankHolidayDates);
  return [ segmentBegin, segmentEnd ];
}

function getOriginationDate(loanDate, isElectronicLoan, bankHolidayDates)
{
  if (isElectronicLoan)
    return skipHolidaysForward(dateAddDays(loanDate, 1), bankHolidayDates);
  else
    return loanDate;
}

function calculateEarliestDueDate(originationDate, minLoanMonths, minLoanDays)
{
  return dateAddDays(dateAddMonths(originationDate, minLoanMonths), minLoanDays);
}

function calculateEarliestScheduleBaseDate(originationDate, minLoanMonths, minLoanDays, holidaySkippingType, bankHolidayDates)
{
  var earliestDueDate = calculateEarliestDueDate(originationDate, minLoanMonths, minLoanDays);
  var holidaySegment = getHolidaySegment(earliestDueDate, bankHolidayDates);
  var segmentSize = dateDifferenceDays(holidaySegment[0], holidaySegment[1]);
  
  switch (holidaySkippingType)
  {
    case 1:
    default:
      return earliestDueDate;
    case 2:
      return holidaySegment[1];
    case 3:
      return holidaySegment[0];
    case 4:
      return dateAddDays(holidaySegment[0], Math.ceil(segmentSize / 2));
    case 5:
      return dateAddDays(holidaySegment[0], Math.floor(segmentSize / 2));
  }
}

function updateBiWeeklySampleDateSetup(weekdayId, sampleDateSelectId, sampleDateNoWeekdaySpanId, sampleDateHiddenId, initFromHidden)
{
  var weekday = document.getElementById(weekdayId).value;
  var sampleDateSelect = document.getElementById(sampleDateSelectId);
  var sampleDateHidden = document.getElementById(sampleDateHiddenId);
  var sampleDateNoWeekdaySpan = document.getElementById(sampleDateNoWeekdaySpanId);

  if (weekday == '')
  {
    sampleDateSelect.style.display = 'none';
    sampleDateNoWeekdaySpan.style.display = '';
  }
  else
  {
    sampleDateSelect.style.display = '';
    sampleDateNoWeekdaySpan.style.display = 'none';
    
    var firstSampleDate = dateServerToday();
    while (DateCalculation_weekdayNames[firstSampleDate.getDay()].toLowerCase() != weekday.toLowerCase())
    {
      firstSampleDate = dateAddDays(firstSampleDate, 1);
    }

    var secondSampleDate = dateAddDays(firstSampleDate, 1);
    while (DateCalculation_weekdayNames[secondSampleDate.getDay()].toLowerCase() != weekday.toLowerCase())
    {
      secondSampleDate = dateAddDays(secondSampleDate, 1);
    }

    while (sampleDateSelect.options.length > 1)
    {
      sampleDateSelect.remove(1);
    }

    var firstOption = document.createElement('option');
    firstOption.value = firstOption.text = formatDate(firstSampleDate);
    DateCalculation_appendOptionToSelect(firstOption, sampleDateSelect);
    var secondOption = document.createElement('option');
    secondOption.value = secondOption.text = formatDate(secondSampleDate);
    DateCalculation_appendOptionToSelect(secondOption, sampleDateSelect);
    
    if (initFromHidden)
    {
      sampleDateSelect.value = sampleDateHidden.value;
    }
    else
    {
      sampleDateSelect.value = '';
    }
  }

  sampleDateHidden.value = sampleDateSelect.value;
}

function applyBiWeeklySampleDate(sampleDateSelectId, sampleDateHiddenId)
{
  var sampleDateSelect = document.getElementById(sampleDateSelectId);
  var sampleDateHidden = document.getElementById(sampleDateHiddenId);
  sampleDateHidden.value = sampleDateSelect.value;
}

function updateBiWeeklySampleDateSetupWithRadio(weekdayId, sampleDateContainerId, firstSampleDateInputId, firstSampleDateLabelId, secondSampleDateInputId, secondSampleDateLabelId, sampleDateNoWeekdaySpanId, sampleDateHiddenId, initFromHidden)
{
  var weekday = document.getElementById(weekdayId).value;
  var sampleDateContainer = document.getElementById(sampleDateContainerId);
  var firstSampleDateInput = document.getElementById(firstSampleDateInputId);
  var secondSampleDateInput = document.getElementById(secondSampleDateInputId);
  var sampleDateNoWeekdaySpan = document.getElementById(sampleDateNoWeekdaySpanId);
  var sampleDateHidden = document.getElementById(sampleDateHiddenId);

  if (weekday == '')
  {
    sampleDateContainer.style.display = 'none';
    sampleDateNoWeekdaySpan.style.display = '';
  }
  else
  {
    sampleDateContainer.style.display = '';
    sampleDateNoWeekdaySpan.style.display = 'none';
    
    var firstSampleDate = dateServerToday();
    while (DateCalculation_weekdayNames[firstSampleDate.getDay()].toLowerCase() != weekday.toLowerCase())
    {
      firstSampleDate = dateAddDays(firstSampleDate, 1);
    }

    var secondSampleDate = dateAddDays(firstSampleDate, 1);
    while (DateCalculation_weekdayNames[secondSampleDate.getDay()].toLowerCase() != weekday.toLowerCase())
    {
      secondSampleDate = dateAddDays(secondSampleDate, 1);
    }

    firstSampleDateInput.value = formatDate(firstSampleDate);
    document.getElementById(firstSampleDateLabelId).innerHTML = formatDate(firstSampleDate);
    secondSampleDateInput.value = formatDate(secondSampleDate);
    document.getElementById(secondSampleDateLabelId).innerHTML = formatDate(secondSampleDate);
    
    if (initFromHidden)
    {
      if (sampleDateHidden.value == formatDate(firstSampleDate))
        firstSampleDateInput.checked = true;
      if (sampleDateHidden.value == formatDate(secondSampleDate))
        secondSampleDateInput.checked = true;
    }
    else
    {
      firstSampleDateInput.checked = secondSampleDateInput.checked = false;
    }
  }

  if (firstSampleDateInput.checked)
    sampleDateHidden.value = firstSampleDateInput.value;
  if (secondSampleDateInput.checked)
    sampleDateHidden.value = secondSampleDateInput.value;
  if ((!firstSampleDateInput.checked) && (!secondSampleDateInput.checked))
    sampleDateHidden.value = '';
}

function applyBiWeeklySampleDateWithRadio(firstSampleDateInputId, secondSampleDateInputId, sampleDateHiddenId)
{
  var firstSampleDateInput = document.getElementById(firstSampleDateInputId);
  var secondSampleDateInput = document.getElementById(secondSampleDateInputId);
  var sampleDateHidden = document.getElementById(sampleDateHiddenId);

  if (firstSampleDateInput.checked)
    sampleDateHidden.value = firstSampleDateInput.value;
  if (secondSampleDateInput.checked)
    sampleDateHidden.value = secondSampleDateInput.value;
}

function applyPaymentTypeFrequencyTypesVisibility(frequencyTypeId, dayInMonthSubtypeId, weeklyContainerIds, biWeeklyContainerIds, monthlyContainerIds, twicePerMonthContainerIds)
{
  var frequencyType = document.getElementById(frequencyTypeId).value;
  
  DateCalculation_hideElements(dayInMonthSubtypeId, weeklyContainerIds, biWeeklyContainerIds, monthlyContainerIds, twicePerMonthContainerIds);
  
  if (frequencyType == '3' || frequencyType == '4')
    DateCalculation_unhideElements(dayInMonthSubtypeId);
  
  if (frequencyType == '1')
    DateCalculation_unhideElements(weeklyContainerIds);
  if (frequencyType == '2')
    DateCalculation_unhideElements(biWeeklyContainerIds);
  if (frequencyType == '3')
    DateCalculation_unhideElements(monthlyContainerIds);
  if (frequencyType == '4')
    DateCalculation_unhideElements(twicePerMonthContainerIds);
}

function applyPaymentTypeDayInMonthSubtype(dayOfMonthRadioId, nthWeekdayRadioId, dayInMonthHiddenId, initFromHidden)
{
  var dayOfMonthRadio = document.getElementById(dayOfMonthRadioId);
  var nthWeekdayRadio = document.getElementById(nthWeekdayRadioId);
  var dayInMonthHidden = document.getElementById(dayInMonthHiddenId);
  
  if (initFromHidden)
  {
    switch (dayInMonthHidden.value)
    {
      case 'dayofmonth':
        dayOfMonthRadio.checked = true;
        nthWeekdayRadio.checked = false;
        break;
      case 'nthweekday':
        dayOfMonthRadio.checked = false;
        nthWeekdayRadio.checked = true;
        break;
    }
  }

  if (dayOfMonthRadio.checked)
  {
    dayInMonthHidden.value = 'dayofmonth';
  }
  else if (nthWeekdayRadio.checked)
  {
    dayInMonthHidden.value = 'nthweekday';
  }
  else
  {
    dayInMonthHidden.value = '';
  }
}

function applyPaymentTypeDayInMonthSubtypesVisibility(dayInMonthSubtypeId, dayOfMonthElementIds, nthWeekdayElementIds)
{
  var dayInMonthSubtype = document.getElementById(dayInMonthSubtypeId).value;
  
  DateCalculation_hideElements(dayOfMonthElementIds, nthWeekdayElementIds);
 
  if (dayInMonthSubtype == 'dayofmonth')
    DateCalculation_unhideElements(dayOfMonthElementIds);
  if (dayInMonthSubtype == 'nthweekday')
    DateCalculation_unhideElements(nthWeekdayElementIds);
}

function applyPaymentTypeDayInMonthSubtypesDimming(dayInMonthSubtypeId, dayOfMonthElementIds, nthWeekdayElementIds)
{
  var dayInMonthSubtype = document.getElementById(dayInMonthSubtypeId).value;
  
  if (!Array.prototype.isPrototypeOf(dayOfMonthElementIds))
  {
    dayOfMonthElementIds = [ dayOfMonthElementIds ];
  }
  if (!Array.prototype.isPrototypeOf(nthWeekdayElementIds))
  {
    nthWeekdayElementIds = [ nthWeekdayElementIds ];
  }
  
  for (var i = 0; i < dayOfMonthElementIds.length; ++i)
  {
    var element = document.getElementById(dayOfMonthElementIds[i]);
    var isActive = (dayInMonthSubtype == 'dayofmonth');
    element.style.color = (isActive ? '' : 'gray');
    if ((element.tagName.toLowerCase() == 'input') || (element.tagName.toLowerCase() == 'select'))
    {
      element.disabled = !isActive;
    }
  }
  
  for (var i = 0; i < nthWeekdayElementIds.length; ++i)
  {
    var element = document.getElementById(nthWeekdayElementIds[i]);
    var isActive = (dayInMonthSubtype == 'nthweekday');
    element.style.color = (isActive ? '' : 'gray');
    if ((element.tagName.toLowerCase() == 'input') || (element.tagName.toLowerCase() == 'select'))
    {
      element.disabled = !isActive;
    }
  }
}

function updateFrequencyBasedSelectDateSetup(dateSelectId, dateHiddenId, messageSpanId,
                                             originationDate, originationDateErrorMessage,
                                             frequencyTypeId, dayInMonthSubtypeId,
                                             frequencyWeekly_WeekdayId,
                                             frequencyBiWeekly_WeekdayId, frequencyBiWeekly_SampleDateId,
                                             frequencyMonthly_DayOrdinalId, frequencyMonthly_WeekdayId, frequencyMonthly_WeekdayOrdinalId,
                                             frequencyTwicePerMonth_FirstDayOrdinalId, frequencyTwicePerMonth_FirstWeekdayId, frequencyTwicePerMonth_FirstWeekdayOrdinalId, frequencyTwicePerMonth_SecondDayOrdinalId, frequencyTwicePerMonth_SecondWeekdayId, frequencyTwicePerMonth_SecondWeekdayOrdinalId,
                                             holidaySkippingType, bankHolidayDates, initFromHidden)
{
  var dateSelect = document.getElementById(dateSelectId);
  var dateHidden = document.getElementById(dateHiddenId);
  var messageSpan = document.getElementById(messageSpanId);
  
  var dateOldValue = dateSelect.value;
  var dateOldCount = dateSelect.options.length;
  
  while (dateSelect.options.length > 1)
  {
    dateSelect.remove(1);
  }
  dateSelect.value = '';
  messageSpan.innerHTML = '';

  if (!getDateObject(originationDate))
  {
    dateHidden.value = '';
    messageSpan.innerHTML = originationDateErrorMessage;
    dateSelect.style.display = 'none';
    return;
  }
  
  var newDateCount = 10 * Math.max(1, Math.floor(dateOldCount / 10));
  if (dateOldValue == 'more')
  {
    newDateCount += 10;
    dateOldValue = '';
  }
  var dates = getPaymentDates(getDateObject(originationDate), newDateCount,
                              frequencyTypeId, dayInMonthSubtypeId,
                              frequencyWeekly_WeekdayId,
                              frequencyBiWeekly_WeekdayId, frequencyBiWeekly_SampleDateId,
                              frequencyMonthly_DayOrdinalId, frequencyMonthly_WeekdayId, frequencyMonthly_WeekdayOrdinalId,
                              frequencyTwicePerMonth_FirstDayOrdinalId, frequencyTwicePerMonth_FirstWeekdayId, frequencyTwicePerMonth_FirstWeekdayOrdinalId, frequencyTwicePerMonth_SecondDayOrdinalId, frequencyTwicePerMonth_SecondWeekdayId, frequencyTwicePerMonth_SecondWeekdayOrdinalId,
                              holidaySkippingType, bankHolidayDates);
  if (dates.length == 0)
  {
    dateHidden.value = '';
    messageSpan.innerHTML = '[No frequency setup]';
    dateSelect.style.display = 'none';
    return;
  }

  for (var i = 0; i < dates.length; ++i)
  {        
    var dateOption = document.createElement('option');
    dateOption.value = dateOption.text = formatDate(dates[i]);
    DateCalculation_appendOptionToSelect(dateOption, dateSelect);
  }

  var moreOption = document.createElement('option');
  moreOption.value = 'more';
  moreOption.text = '-- More --';
  DateCalculation_appendOptionToSelect(moreOption, dateSelect);
  
  dateSelect.style.display = '';

  if (initFromHidden)
  {
    dateSelect.value = dateHidden.value;
  }
  else
  {
    dateSelect.value = dateOldValue;
    if (dateSelect.value != dateOldValue) dateSelect.value = '';
    dateHidden.value = dateSelect.value;
  }
}

function getPaymentDates(baseDateObj, count, 
                         frequencyTypeId, dayInMonthSubtypeId,
                         frequencyWeekly_WeekdayId,
                         frequencyBiWeekly_WeekdayId, frequencyBiWeekly_SampleDateId,
                         frequencyMonthly_DayOrdinalId, frequencyMonthly_WeekdayId, frequencyMonthly_WeekdayOrdinalId,
                         frequencyTwicePerMonth_FirstDayOrdinalId, frequencyTwicePerMonth_FirstWeekdayId, frequencyTwicePerMonth_FirstWeekdayOrdinalId, frequencyTwicePerMonth_SecondDayOrdinalId, frequencyTwicePerMonth_SecondWeekdayId, frequencyTwicePerMonth_SecondWeekdayOrdinalId,
                         holidaySkippingType, bankHolidayDates)
{
  var frequencyType = document.getElementById(frequencyTypeId).value;
  var dayInMonthSubtype = document.getElementById(dayInMonthSubtypeId).value;

  var dates = [];
  switch (frequencyType)
  {
    case '1':
      var weekdayName = document.getElementById(frequencyWeekly_WeekdayId).value;
      if (!weekdayName) return [];
      var firstWeekdayDate = DateCalculation_getEarliestWeekdaySince(baseDateObj, weekdayName);
      
      // Adjust for holiday skipping effects - backward.
      while (skipHolidays(dateAddDays(firstWeekdayDate, -7), holidaySkippingType, bankHolidayDates)
             >= baseDateObj)
      {
        firstWeekdayDate = dateAddDays(firstWeekdayDate, -7);
      }
      // Adjust for holiday skipping effects - forward.
      while (skipHolidays(dateAddDays(firstWeekdayDate), holidaySkippingType, bankHolidayDates)
             < baseDateObj)
      {
        firstWeekdayDate = dateAddDays(firstWeekdayDate, 7);
      }

      var date = firstWeekdayDate;
      var lastAddedDateSecs = null;
      for (var i = 0; i < count; ++i)
      {
        var dateWithSkipping = skipHolidays(date, holidaySkippingType, bankHolidayDates);
        if (dateWithSkipping.getTime() != lastAddedDateSecs)
        {
          dates.push(dateWithSkipping);
          lastAddedDateSecs = dateWithSkipping.getTime();
        }
        date = dateAddDays(date, 7);
      }
      break;

    case '2':
      var weekdayName = document.getElementById(frequencyBiWeekly_WeekdayId).value;
      var sampleDateString = document.getElementById(frequencyBiWeekly_SampleDateId).value;
      if (!weekdayName) return [];
      if (!getDateObject(sampleDateString)) return [];
      var firstWeekdayDate = DateCalculation_getEarliestWeekdaySince(baseDateObj, weekdayName);
      var firstBiWeeklyDate;
      if (dateDifferenceDays(getDateObject(sampleDateString), firstWeekdayDate) % 14 != 0)
      {
        firstBiWeeklyDate = dateAddDays(firstWeekdayDate, 7);
      }
      else
      {
        firstBiWeeklyDate = firstWeekdayDate;
      }
      
      // Adjust for holiday skipping effects - backward.
      while (skipHolidays(dateAddDays(firstBiWeeklyDate, -14), holidaySkippingType, bankHolidayDates)
             >= baseDateObj)
      {
        firstBiWeeklyDate = dateAddDays(firstBiWeeklyDate, -14);
      }
      // Adjust for holiday skipping effects - forward.
      while (skipHolidays(dateAddDays(firstBiWeeklyDate), holidaySkippingType, bankHolidayDates)
             < baseDateObj)
      {
        firstBiWeeklyDate = dateAddDays(firstBiWeeklyDate, 14);
      }

      var date = firstBiWeeklyDate;
      for (var i = 0; i < count; ++i)
      {
        var dateWithSkipping = skipHolidays(date, holidaySkippingType, bankHolidayDates);
        if (dateWithSkipping.getTime() != lastAddedDateSecs)
        {
          dates.push(dateWithSkipping);
          lastAddedDateSecs = dateWithSkipping.getTime();
        }
        date = dateAddDays(date, 14);
      }
      break;
      
    case '3':
      if (!dayInMonthSubtype) return [];
      var dayOrdinal = document.getElementById(frequencyMonthly_DayOrdinalId).value;
      var weekdayName = document.getElementById(frequencyMonthly_WeekdayId).value;
      var weekdayOrdinal = document.getElementById(frequencyMonthly_WeekdayOrdinalId).value;
      
      var monthDate = new Date(baseDateObj);
      monthDate.setDate(1);
      // To account for possible holiday skipping effects, we go back to one month.
      // Supposing that holiday skipping won't move dates that far in time.
      monthDate.setMonth(monthDate.getMonth() - 1);
      while (dates.length < count)
      {
        var candidateDate = DateCalculation_getDayInMonth(monthDate, dayInMonthSubtype, dayOrdinal, weekdayName, weekdayOrdinal);
        var dateWithSkipping = skipHolidays(candidateDate, holidaySkippingType, bankHolidayDates);
        if (!dateWithSkipping) return [];
        if (dateWithSkipping >= baseDateObj) dates.push(dateWithSkipping);
        monthDate.setMonth(monthDate.getMonth() + 1);
      }
      break;
    
    case '4':
      if (!dayInMonthSubtype) return [];
      var firstDayOrdinal = document.getElementById(frequencyTwicePerMonth_FirstDayOrdinalId).value;
      var firstWeekdayName = document.getElementById(frequencyTwicePerMonth_FirstWeekdayId).value;
      var firstWeekdayOrdinal = document.getElementById(frequencyTwicePerMonth_FirstWeekdayOrdinalId).value;
      var secondDayOrdinal = document.getElementById(frequencyTwicePerMonth_SecondDayOrdinalId).value;
      var secondWeekdayName = document.getElementById(frequencyTwicePerMonth_SecondWeekdayId).value;
      var secondWeekdayOrdinal = document.getElementById(frequencyTwicePerMonth_SecondWeekdayOrdinalId).value;
      
      var monthDate = new Date(baseDateObj);
      monthDate.setDate(1);
      // To account for possible holiday skipping effects, we go back to one month.
      // Supposing that holiday skipping won't move dates that far in time.
      monthDate.setMonth(monthDate.getMonth() - 1);
      while (true)
      {
        var candidateDate;
        var dateWithSkipping;
        
        if (dates.length == count) break;
        candidateDate = DateCalculation_getDayInMonth(monthDate, dayInMonthSubtype, firstDayOrdinal, firstWeekdayName, firstWeekdayOrdinal);
        dateWithSkipping = skipHolidays(candidateDate, holidaySkippingType, bankHolidayDates);
        if (!dateWithSkipping) return [];
        if (dateWithSkipping >= baseDateObj) dates.push(dateWithSkipping);

        if (dates.length == count) break;
        candidateDate = DateCalculation_getDayInMonth(monthDate, dayInMonthSubtype, secondDayOrdinal, secondWeekdayName, secondWeekdayOrdinal);
        dateWithSkipping = skipHolidays(candidateDate, holidaySkippingType, bankHolidayDates);
        if (!dateWithSkipping) return [];
        if (dateWithSkipping >= baseDateObj) dates.push(dateWithSkipping);
        
        monthDate.setMonth(monthDate.getMonth() + 1);
      }
      break;
    
    default:
      return [];
  }
  
  return dates;
}

function makePaymentFrequencyDescription(frequencyTypeId, dayInMonthSubtypeId,
                                         frequencyWeekly_WeekdayId,
                                         frequencyBiWeekly_WeekdayId, frequencyBiWeekly_SampleDateId,
                                         frequencyMonthly_DayOrdinalId, frequencyMonthly_WeekdayId, frequencyMonthly_WeekdayOrdinalId,
                                         frequencyTwicePerMonth_FirstDayOrdinalId, frequencyTwicePerMonth_FirstWeekdayId, frequencyTwicePerMonth_FirstWeekdayOrdinalId, frequencyTwicePerMonth_SecondDayOrdinalId, frequencyTwicePerMonth_SecondWeekdayId, frequencyTwicePerMonth_SecondWeekdayOrdinalId)
{
  var frequencyType = document.getElementById(frequencyTypeId).value;
  var dayInMonthSubtype = document.getElementById(dayInMonthSubtypeId).value;

  switch (frequencyType)
  {
    case '1':
      var weekdayName = document.getElementById(frequencyWeekly_WeekdayId).value;
      if (!weekdayName) return '';
      return 'Each ' + DateCalculation_firstLetterToUppercase(weekdayName);

    case '2':
      var weekdayName = document.getElementById(frequencyBiWeekly_WeekdayId).value;
      var sampleDateString = document.getElementById(frequencyBiWeekly_SampleDateId).value;
      if (!weekdayName) return '';
      if (!getDateObject(sampleDateString)) return '';
      var firstWeekdayDate = DateCalculation_getEarliestWeekdaySince(dateServerToday(), weekdayName);
      var firstBiWeeklyDate;
      if (dateDifferenceDays(getDateObject(sampleDateString), firstWeekdayDate) % 14 != 0)
      {
        firstBiWeeklyDate = dateAddDays(firstWeekdayDate, 7);
      }
      else
      {
        firstBiWeeklyDate = firstWeekdayDate;
      }

      return 'Each other ' + DateCalculation_firstLetterToUppercase(weekdayName)
             + ' (e.g. ' + formatDate(firstBiWeeklyDate) + ')';
      
    case '3':
      if (!dayInMonthSubtype) return '';
      var dayOrdinal = document.getElementById(frequencyMonthly_DayOrdinalId).value;
      var weekdayName = document.getElementById(frequencyMonthly_WeekdayId).value;
      var weekdayOrdinal = document.getElementById(frequencyMonthly_WeekdayOrdinalId).value;
      
      var dayInMonthDescription = DateCalculation_makeDayInMonthDescription(dayInMonthSubtype, dayOrdinal, weekdayName, weekdayOrdinal);
      if (dayInMonthDescription == '') return '';
      
      return 'The ' + dayInMonthDescription + ' of each month';
    
    case '4':
      if (!dayInMonthSubtype) return '';
      var firstDayOrdinal = document.getElementById(frequencyTwicePerMonth_FirstDayOrdinalId).value;
      var firstWeekdayName = document.getElementById(frequencyTwicePerMonth_FirstWeekdayId).value;
      var firstWeekdayOrdinal = document.getElementById(frequencyTwicePerMonth_FirstWeekdayOrdinalId).value;
      var secondDayOrdinal = document.getElementById(frequencyTwicePerMonth_SecondDayOrdinalId).value;
      var secondWeekdayName = document.getElementById(frequencyTwicePerMonth_SecondWeekdayId).value;
      var secondWeekdayOrdinal = document.getElementById(frequencyTwicePerMonth_SecondWeekdayOrdinalId).value;

      var firstDayDescription = DateCalculation_makeDayInMonthDescription(dayInMonthSubtype, firstDayOrdinal, firstWeekdayName, firstWeekdayOrdinal);
      if (firstDayDescription == '') return '';
      var secondDayDescription = DateCalculation_makeDayInMonthDescription(dayInMonthSubtype, secondDayOrdinal, secondWeekdayName, secondWeekdayOrdinal);
      if (secondDayDescription == '') return '';
 
      return 'The ' + firstDayDescription + ' and ' + secondDayDescription + ' of each month';
    
    default:
      return '';
  }
}

function makePaymentFrequencyParametersString(frequencyTypeId, dayInMonthSubtypeId,
                                              frequencyWeekly_WeekdayId,
                                              frequencyBiWeekly_WeekdayId, frequencyBiWeekly_SampleDateId,
                                              frequencyMonthly_DayOrdinalId, frequencyMonthly_WeekdayId, frequencyMonthly_WeekdayOrdinalId,
                                              frequencyTwicePerMonth_FirstDayOrdinalId, frequencyTwicePerMonth_FirstWeekdayId, frequencyTwicePerMonth_FirstWeekdayOrdinalId, frequencyTwicePerMonth_SecondDayOrdinalId, frequencyTwicePerMonth_SecondWeekdayId, frequencyTwicePerMonth_SecondWeekdayOrdinalId)
{
  var frequencyType = document.getElementById(frequencyTypeId).value;
  var dayInMonthSubtype = document.getElementById(dayInMonthSubtypeId).value;

  switch (frequencyType)
  {
    case '1':
      var weekdayName = document.getElementById(frequencyWeekly_WeekdayId).value;
      if (!weekdayName) return '';
      return 'weekday:' + weekdayName.toLowerCase();

    case '2':
      var weekdayName = document.getElementById(frequencyBiWeekly_WeekdayId).value;
      var sampleDateString = document.getElementById(frequencyBiWeekly_SampleDateId).value;
      if (!weekdayName) return '';
      if (!getDateObject(sampleDateString)) return '';
      var daysSinceY2k = dateDifferenceDays(new Date('1/1/2000'), getDateObject(sampleDateString));
      var oddWeeksSinceY2k = ((14 + daysSinceY2k % 14) % 14 >= 7);
      return 'weekday:' + weekdayName.toLowerCase() + '|odd:' + (oddWeeksSinceY2k ? 'yes' : 'no');
      
    case '3':
      if (!dayInMonthSubtype) return '';
      var dayOrdinal = document.getElementById(frequencyMonthly_DayOrdinalId).value;
      var weekdayName = document.getElementById(frequencyMonthly_WeekdayId).value;
      var weekdayOrdinal = document.getElementById(frequencyMonthly_WeekdayOrdinalId).value;
      
      var dayInMonthParameterString = DateCalculation_makeDayInMonthParameterString(dayInMonthSubtype, dayOrdinal, weekdayName, weekdayOrdinal);
      if (dayInMonthParameterString == '') return '';
      
      return 'subtype:' + dayInMonthSubtype + '|day:' + dayInMonthParameterString;
    
    case '4':
      if (!dayInMonthSubtype) return '';
      var firstDayOrdinal = document.getElementById(frequencyTwicePerMonth_FirstDayOrdinalId).value;
      var firstWeekdayName = document.getElementById(frequencyTwicePerMonth_FirstWeekdayId).value;
      var firstWeekdayOrdinal = document.getElementById(frequencyTwicePerMonth_FirstWeekdayOrdinalId).value;
      var secondDayOrdinal = document.getElementById(frequencyTwicePerMonth_SecondDayOrdinalId).value;
      var secondWeekdayName = document.getElementById(frequencyTwicePerMonth_SecondWeekdayId).value;
      var secondWeekdayOrdinal = document.getElementById(frequencyTwicePerMonth_SecondWeekdayOrdinalId).value;

      var firstDayParameterString = DateCalculation_makeDayInMonthParameterString(dayInMonthSubtype, firstDayOrdinal, firstWeekdayName, firstWeekdayOrdinal);
      if (firstDayParameterString == '') return '';
      var secondDayParameterString = DateCalculation_makeDayInMonthParameterString(dayInMonthSubtype, secondDayOrdinal, secondWeekdayName, secondWeekdayOrdinal);
      if (secondDayParameterString == '') return '';
 
      return 'subtype:' + dayInMonthSubtype + '|firstday:' + firstDayParameterString + '|secondday:' + secondDayParameterString;
    
    default:
      return '';
  }
}

function DateCalculation_setDisplayVisibility(idOrArray, isVisible)
{
  if (Array.prototype.isPrototypeOf(idOrArray))
  {
    for (var i = 0; i < idOrArray.length; ++i)
    {
      document.getElementById(idOrArray[i]).style.display = (isVisible ? '' : 'none');
      if (isVisible)
        DateCalculation_removeClass(document.getElementById(idOrArray[i]), 'ForceHidden');
      else
        DateCalculation_addClass(document.getElementById(idOrArray[i]), 'ForceHidden');
    }
  }
  else
  {
    document.getElementById(idOrArray).style.display = (isVisible ? '' : 'none');
    if (isVisible)
      DateCalculation_removeClass(document.getElementById(idOrArray), 'ForceHidden');
    else
      DateCalculation_addClass(document.getElementById(idOrArray), 'ForceHidden');
  }
}

function DateCalculation_hideElements(/* idOrArray, ...*/)
{
  for (var k = 0; k < arguments.length; ++k)
    {
    var idOrArray = arguments[k];
    if (!Array.prototype.isPrototypeOf(idOrArray)) idOrArray = [ idOrArray ];

    for (var i = 0; i < idOrArray.length; ++i)
    {
      document.getElementById(idOrArray[i]).style.display = 'none';
      DateCalculation_addClass(document.getElementById(idOrArray[i]), 'ForceHidden');
    }
  }
}

function DateCalculation_unhideElements(/* idOrArray, ...*/)
{
  for (var k = 0; k < arguments.length; ++k)
    {
    var idOrArray = arguments[k];
    if (!Array.prototype.isPrototypeOf(idOrArray)) idOrArray = [ idOrArray ];

    for (var i = 0; i < idOrArray.length; ++i)
    {
      document.getElementById(idOrArray[i]).style.display = '';
      DateCalculation_removeClass(document.getElementById(idOrArray[i]), 'ForceHidden');
    }
  }
}

function DateCalculation_addClass(element, className)
{
  if (!new RegExp(' ?\\b' + className + '\\b', 'g').test(element.className))
    element.className = element.className + ' ' + className;
}

function DateCalculation_removeClass(element, className)
{
  element.className = element.className.replace(new RegExp(' ?\\b' + className + '\\b', 'g'), '');
}

function DateCalculation_appendOptionToSelect(option, select)
{
  // IE doesn't accept null, and FF doesn't accept -1 or options.length.
  // So we have to have two variants. :(
  try {
    select.add(option, null);
  } catch(ex) {
    select.add(option);
  }
}

function DateCalculation_getEarliestWeekdaySince(date, weekdayName)
{
  while (DateCalculation_weekdayNames[date.getDay()].toLowerCase() != weekdayName.toLowerCase())
  {
    date = dateAddDays(date, 1);
  }
  return date;
}

function DateCalculation_getDayInMonth(monthDate, dayInMonthSubtype, dayOrdinal, weekdayName, weekdayOrdinal)
{
  switch (dayInMonthSubtype)
  {
    case 'dayofmonth':
      if (dayOrdinal == '') return false;
      
      var date = new Date(monthDate);
      if (dayOrdinal == -1)
      {
        date.setDate(31);
      }
      else
      {
        date.setDate(dayOrdinal);
      }
      while (date.getMonth() != monthDate.getMonth())
      {
        date = dateAddDays(date, -1);
      }
      return date;
      
    case 'nthweekday':
      if ((weekdayName == '') || (weekdayOrdinal == '')) return false;

      if (weekdayOrdinal == -1)
      {
        var nextMonthDate = new Date(monthDate);
        nextMonthDate.setMonth(nextMonthDate.getMonth() + 1);
        var firstWeekdayInNextMonth = DateCalculation_getEarliestWeekdaySince(nextMonthDate, weekdayName);
        return dateAddDays(firstWeekdayInNextMonth, -7);
      }
      else
      {
        var firstWeekdayInMonth = DateCalculation_getEarliestWeekdaySince(monthDate, weekdayName);
        return dateAddDays(firstWeekdayInMonth, 7 * (weekdayOrdinal - 1));
      }
      
    default:
      return false;
  }
}

function DateCalculation_firstLetterToUppercase(str)
{
  if (!str) return str;
  return str.substr(0, 1).toUpperCase() + str.substr(1);
}

function DateCalculation_makeDayInMonthDescription(dayInMonthSubtype, dayOrdinal, weekdayName, weekdayOrdinal)
{
  switch (dayInMonthSubtype)
  {
    case 'dayofmonth':
      if (dayOrdinal == '') return '';
      return (dayOrdinal == -1 ? 'Last' : DateCalculation_getDayOrdinalString(dayOrdinal));
    case 'nthweekday':
      if ((weekdayName == '') || (weekdayOrdinal == '')) return '';

      var ordinalAdjective;
      switch (weekdayOrdinal)
      {
        case '1':
          ordinalAdjective = 'First';
          break;
        case '2':
          ordinalAdjective = 'Second';
          break;
        case '3':
          ordinalAdjective = 'Third';
          break;
        case '4':
          ordinalAdjective = 'Fourth';
          break;
        case '-1':
          ordinalAdjective = 'Last';
          break;
        default:
          return '';
      }

      return ordinalAdjective + ' ' + DateCalculation_firstLetterToUppercase(weekdayName);
    default:
      return '';
  }
}

function DateCalculation_makeDayInMonthParameterString(dayInMonthSubtype, dayOrdinal, weekdayName, weekdayOrdinal)
{
  switch (dayInMonthSubtype)
  {
    case 'dayofmonth':
      if (dayOrdinal == '') return '';
      return (dayOrdinal == -1 ? 'last' : dayOrdinal);
    case 'nthweekday':
      if ((weekdayName == '') || (weekdayOrdinal == '')) return '';

      if (weekdayOrdinal == -1)
      {
        return weekdayName.toLowerCase() + ',last';
      }
      else
      {
        return weekdayName.toLowerCase() + ',' + weekdayOrdinal;
      }
    default:
      return '';
  }
}

function DateCalculation_getDayOrdinalString(dayOrdinal)
{
  var suffix;
  switch (dayOrdinal % 10)
  {
      case 0:
      case 4:
      case 5:
      case 6:
      case 7:
      case 8:
      case 9:
          suffix = 'th';
          break;
      case 1:
          suffix = (dayOrdinal == 11 ? 'th' : 'st');
          break;
      case 2:
          suffix = (dayOrdinal == 12 ? 'th' : 'nd');
          break;
      case 3:
          suffix = (dayOrdinal == 13 ? 'th' : 'rd');
          break;
      default:
          return '';
  }

  return dayOrdinal + suffix;
}

