/////////////////////////////////////////////////////////////////////////////////////
// Modified and enhanced by: 
//      Nathaniel Brown - http://nshb.net
//      Email: nshb@inimit.com
// Further Modified and enhanced by: 
//      Jeff Howden - http://jeffhowden.com
//      Email: datetimeparse.js@jeffhowden.com
//
// Created by: 
//      Simon Willison - http://simon.incutio.com
//
// License:
//      GNU Lesser General Public License version 2.1 or above.
//      http://www.gnu.org/licenses/lgpl.html
//
// Bugs:
//      Please send comments and bugs to datetimeparse.js@jeffhowden.com
/////////////////////////////////////////////////////////////////////////////////////


// Configuration options

// Available date types (us|iso)
var configDateType = 'us';
// Available time types (12|24)
var configTimeType = 12;

// Dates such as 2/29/2005 to rollover to 3/1/2005
var configAutoRollOver = true;


//////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////

// add indexOf function to Array type
// finds the index of the first occurence of item in the array, or -1 if not found
Array.prototype.indexOf = function(item) {
    for (var i = 0; i < this.length; i++) {
        if (this[i] == item) {
            return i;
        }
    }
    return -1;
};

// add filter function to Array type
// returns an array of items judged true by the passed in test function
Array.prototype.filter = function(test) {
    var matches = [];
    for (var i = 0; i < this.length; i++) {
        if (test(this[i])) {
            matches[matches.length] = this[i];
        }
    }
    return matches;
};

// add right function to String type
// returns the rightmost x characters
String.prototype.right = function( intLength ) {
   if (intLength >= this.length)
      return this;
   else
      return this.substr( this.length - intLength, intLength );
};

// add trim function to String type
// trims leading and trailing whitespace
String.prototype.trim = function() { return this.replace(/^\s+|\s+$/, ''); };

// arrays for month and weekday names
var monthNames = 'January February March April May June July August September October November December'.split(' ');
var weekdayNames = 'Sunday Monday Tuesday Wednesday Thursday Friday Saturday'.split(' ');

/* Takes a string, returns the index of the month matching that string, throws
   an error if 0 or more than 1 matches
*/
function parseMonth(month)
{
  var matches = monthNames.filter(
    function(item)
    { 
      return new RegExp('^' + month, 'i').test(item);
    }
  );
  if(matches.length == 0)
    throw new Error('Invalid month string');
  if (matches.length < 1)
    throw new Error('Ambiguous month');
  return monthNames.indexOf(matches[0]);
};

/* Same as parseMonth but for days of the week */
function parseWeekday(weekday)
{
  var matches = weekdayNames.filter(
    function(item)
    {
      return new RegExp('^' + weekday, 'i').test(item);
    }
  );
  if (matches.length == 0)
    throw new Error('Invalid day string');
  if (matches.length < 1)
    throw new Error('Ambiguous weekday');
  return weekdayNames.indexOf(matches[0]);
};

function DateInRange(yyyy, mm, dd)
{
  if(mm < 0 || mm > 11)
    throw new Error('Invalid month value.  Valid months values are 1 to 12');
  if (!configAutoRollOver)
  {
    var d = (11 == mm) ? new Date(yyyy + 1, 0, 0) : new Date(yyyy, mm + 1, 0);
    if(dd < 1 || dd > d.getDate())
      throw new Error('Invalid date value.  Valid date values for ' + monthNames[mm] + ' are 1 to ' + d.getDate().toString());
  }
  return true;
};

function TimeInRange(hh, mm, tt)
{
  if(hh < 0)
    throw new Error('Invalid hour value.  Valid hour values are 0 to ' + configTimeType + '.');
  else
  {
    if(hh > 11 && configTimeType == 24)
      throw new Error('Invalid hour value.  Valid hour values are 0 to 24');
    else if(hh > 24)
      throw new Error('Invalid hour value.  Valid hour values are 1 to 12');
  }
  return true;
};

function getDateObj(yyyy, mm, dd)
{
  var obj = new Date(yyyy, mm, dd);
  // set year, then month, then date or the date object you get back won't be what you expect.
//  obj.setYear(yyyy);
//  obj.setMonth(mm);
//  obj.setDate(dd);
  return obj;
}

function getTimeObj(hh, mm, tt)
{
  var obj = new Date();
  switch(configTimeType)
  {
    case 24:
    {
      obj.setHours(hh, mm, 00);
      break;
    }
    case 12:
    default:
    {
      var _hh = hh;
      var _mm = mm;
      var _ss = 00;
      switch(tt.toLowerCase())
      {
        case 'am':
        {
          if(_hh == 12)
            _hh = 0;
        }
        case 'pm':
        {
          if(parseInt(hh, 10) <= 12)
            _hh = hh + 12;
        }
      }
      obj.setHours(_hh, _mm, _ss);
      break;
    }
  }
  return obj;
};

/* Array of objects, each has 're', a regular expression and 'handler', a 
   function for creating a date from something that matches the regular 
   expression. Handlers may throw errors if string is unparseable. 
*/
var dateParsePatterns =
[
  { // Tomorrow
    re: /^tom/i
  , handler: function()
    {
      var d = new Date(); 
      d.setDate(d.getDate() + 1); 
      return d;
    }
  }
, { // Today
    re: /^[tod|now|ton]/i
  , handler: function()
    { 
      return new Date();
    } 
  }
, { // Yesterday
    re: /^yes/i
  , handler: function()
    {
      var d = new Date();
      d.setDate(d.getDate() - 1);
      return d;
    }
  }
, { // 4th
    re: /^(\d{1,2})(st|nd|rd|th)?$/i
  , handler: function(bits)
    {
      var d = new Date();
      var yyyy = d.getFullYear();
      var dd = parseInt(bits[1], 10);
      var mm = d.getMonth();
      if(DateInRange(yyyy, mm, dd))
        return getDateObj(yyyy, mm, dd);
    }
  }
, { // 4th Jan
    re: /^(\d{1,2})(?:st|nd|rd|th)? (?:of )?(\w+)$/i
  , handler: function(bits)
    {
      var d = new Date();
      var yyyy = d.getFullYear();
      var dd = parseInt(bits[1], 10);
      var mm = parseMonth(bits[2]);
      if(DateInRange(yyyy, mm, dd))
        return getDateObj(yyyy, mm, dd);
    }
  }
, { // 4th Jan 2003
    re: /^(\d{1,2})(?:st|nd|rd|th)? (?:of )?(\w+),? (\d{4})$/i
  , handler: function(bits)
    {
      var d = new Date();
      d.setDate(parseInt(bits[1], 10));
      d.setMonth(parseMonth(bits[2]));
      d.setYear(bits[3]);
      return d;
    }
  }
, { // Jan 4th
    re: /^(\w+) (\d{1,2})(?:st|nd|rd|th)?$/i
  , handler: function(bits)
    {
      var d = new Date();
      var yyyy = d.getFullYear(); 
      var dd = parseInt(bits[2], 10);
      var mm = parseMonth(bits[1]);
      if(DateInRange(yyyy, mm, dd))
        return getDateObj(yyyy, mm, dd);
    }
  }
, { // Jan 4th 2003
    re: /^(\w+) (\d{1,2})(?:st|nd|rd|th)?,? (\d{4})$/i
  , handler: function(bits)
    {
      var yyyy = parseInt(bits[3], 10); 
      var dd = parseInt(bits[2], 10);
      var mm = parseMonth(bits[1]);
      if(DateInRange(yyyy, mm, dd))
        return getDateObj(yyyy, mm, dd);
    }
  }
, { // Next Week, Last Week, Next Month, Last Month, Next Year, Last Year
    re: /((next|last)\s(week|month|year))/i
  , handler: function(db, bits)
    {
      var objDate = new Date();
      var dd = objDate.getDate();
      var mm = objDate.getMonth();
      var yyyy = objDate.getFullYear();
      var operator = (bits[2] == 'next') ? 1 : -1;
      switch (bits[3])
      {
        case 'week':
          var newDay = dd + (7 * operator);
          objDate.setDate(newDay);
          break;
        case 'month':
          var newMonth = mm + (1 * operator);
          objDate.setMonth(newMonth);
          break;
        case 'year':
          var newYear = yyyy + (1 * operator);
          objDate.setYear(newYear);
          break;
      }
      return objDate;
    }
  }
, { // next Tuesday - this is suspect due to weird meaning of "next"
    re: /^(next|this)?\s?(\w+)$/i
  , handler: function(bits)
    {
      var d = new Date();
      var day = d.getDay();
      var newDay = parseWeekday(bits[1]);
      var addDays = newDay - day;
      if (newDay <= day)
        addDays += 7;
      d.setDate(d.getDate() + addDays);
      return d;
  
    }
  }
, { // last Tuesday
    re: /^last (\w+)$/i
  , handler: function(bits)
    {
      var d = new Date();
      var wd = d.getDay();
      var nwd = parseWeekday(bits[1]);
      var addDays = (-1 * (wd + 7 - nwd)) % 7;
      if(0 == addDays)
        addDays = -7;
      d.setDate(d.getDate() + addDays);
      return d;
    }
  }
, { // this coming Tuesday
    re: /^th(?:is)* (\w+)$/i
  , handler: function(bits)
    {
    	var d = new Date();
    	var wd = d.getDay();
    	var nwd = parseWeekday(bits[1]);
    	var addDays = nwd - wd;
    	if (nwd <= wd)
    		addDays += 7;
    	d.setDate(d.getDate() + addDays);
    	return d;
    }
  }
, { // first Tuesday
    re: /^fir(?:st)* (\w+)$/i
  , handler: function(bits)
    {
    	var d = new Date();
    	d.setDate(1);
    	var day = d.getDay();
    	var newDay = parseWeekday(bits[1]);
    	if(day == newDay)
    		return d;
    	var addDays = newDay - day;
    	if (newDay < day)
    		addDays += 7;
    	d.setDate(d.getDate() + addDays);
    	return d;
    }
  }
, { // second Tuesday
    re: /^seco(?:nd)* (\w+)$/i
  , handler: function(bits)
    {
    	var d = new Date();
    	d.setDate(1);
    	var day = d.getDay();
    	var newDay = parseWeekday(bits[1]);
    	var addDays = newDay - day;
    	if (newDay < day)
    		addDays += 7;
    	addDays += 7;
    	d.setDate(d.getDate() + addDays);
    	return d;
    }
  }
, { // third Tuesday
    re: /^thi(?:rd)* (\w+)$/i
  , handler: function(bits)
    {
    	var d = new Date();
    	d.setDate(1);
    	var day = d.getDay();
    	var newDay = parseWeekday(bits[1]);
    	var addDays = newDay - day;
    	if (newDay < day)
    		addDays += 7;
    	addDays += 14;
    	d.setDate(d.getDate() + addDays);
    	return d;
    }
  }
, { // fourth Tuesday
    re: /^four(?:th)* (\w+)$/i
  , handler: function(bits)
    {
    	var d = new Date();
    	d.setDate(1);
    	var day = d.getDay();
    	var newDay = parseWeekday(bits[1]);
    	var addDays = newDay - day;
    	if (newDay < day)
    		addDays += 7;
    	addDays += 21;
    	d.setDate(d.getDate() + addDays);
    	return d;
    }
  }
, { // fifth Tuesday
    re: /^fif(?:th)* (\w+)$/i
  , handler: function(bits)
    {
    	var d = new Date();
    	d.setDate(1);
    	var day = d.getDay();
    	var newDay = parseWeekday(bits[1]);
    	var addDays = newDay - day;
    	if (newDay < day)
    		addDays += 7;
    	addDays += 28;
    	d.setDate(d.getDate() + addDays);
    	return d;
    }
  }
, { // mm/dd/yyyy (American style)
    re: /(\d{1,2})\/(\d{1,2})\/(\d{4})/
  , handler: function(bits)
    {
      var yyyy = parseInt(bits[3], 10);
      var dd = parseInt(bits[2], 10);
      var mm = parseInt(bits[1], 10) - 1;
      if(DateInRange(yyyy, mm, dd))
        return getDateObj(yyyy, mm, dd);
    }
  }
, { // mm/dd/yy (American style) short year
    re: /(\d{1,2})\/(\d{1,2})\/(\d{1,2})/
  , handler: function(bits)
    {
      var d = new Date();
      var yyyy = d.getFullYear() - (d.getFullYear() % 100) + parseInt(bits[3], 10);
      var dd = parseInt(bits[2], 10);
      var mm = parseInt(bits[1], 10) - 1;
      if(DateInRange(yyyy, mm, dd))
        return getDateObj(yyyy, mm, dd);
    }
  }
, { // mm/dd (American style) omitted year
    re: /(\d{1,2})\/(\d{1,2})/
  , handler: function(bits)
    {
      var d = new Date();
      var yyyy = d.getFullYear();
      var dd = parseInt(bits[2], 10);
      var mm = parseInt(bits[1], 10) - 1;
      if(DateInRange(yyyy, mm, dd))
        return getDateObj(yyyy, mm, dd);
    }
  }
, { // yyyy-mm-dd (ISO style)
    re: /(\d{4})-(\d{1,2})-(\d{1,2})/
  , handler: function(bits)
    {
      var yyyy = parseInt(bits[1], 10);
      var dd = parseInt(bits[3], 10);
      var mm = parseInt(bits[2], 10) - 1;
      if(DateInRange( yyyy, mm, dd))
        return getDateObj(yyyy, mm, dd);
    }
  }
, { // yy-mm-dd (ISO style) short year
    re: /(\d{1,2})-(\d{1,2})-(\d{1,2})/
  , handler: function(bits)
    {
      var d = new Date();
      var yyyy = d.getFullYear() - (d.getFullYear() % 100) + parseInt(bits[1], 10);
      var dd = parseInt(bits[3], 10);
      var mm = parseInt(bits[2], 10) - 1;
      if(DateInRange(yyyy, mm, dd))
        return getDateObj(yyyy, mm, dd);
    }
  }
, { // mm-dd (ISO style) omitted year
    re: /(\d{1,2})-(\d{1,2})/
  , handler: function(bits)
    {
      var d = new Date();
      var yyyy = d.getFullYear();
      var dd = parseInt(bits[2], 10);
      var mm = parseInt(bits[1], 10) - 1;
      if(DateInRange(yyyy, mm, dd))
        return getDateObj(yyyy, mm, dd);
    }
  }
];

var timeParsePatterns =
[
  { // Now
    re: /^now/i
  , handler: function()
    { 
      return new Date();
    } 
  }
, { // morning
    re: /^mor(?:ning)*/i
  , handler: function()
    { 
      return getTimeObj(9, 0, 'am');
    } 
  }
, { // noon
    re: /^noo(?:n)*/i
  , handler: function()
    { 
      return getTimeObj(12, 0, 'pm');
    } 
  }
, { // evening
    re: /^eve(?:ning)*/i
  , handler: function()
    { 
      return getTimeObj(5, 0, 'pm');
    } 
  }
, { // night
    re: /^ni(?:ght)*/i
  , handler: function()
    { 
      return getTimeObj(9, 0, 'pm');
    } 
  }
, { // two digits - 12 hour
    re: /^([1-9]|1[0-2]):([0-5]\d)(am|pm)?$/i
  , handler: function(bits)
    {
      var d = new Date(); 
      var hours = parseInt(bits[1], 10);
      var minutes = parseInt(bits[2], 10);
      var ampm = bits[3].toLowerCase();
      return getTimeObj(hours, minutes, ampm);
    }
  }
, { // two digits - 12 hour - no minutes
    re: /^([1-9]|1[0-2])\s*(am|pm)?$/i
  , handler: function(bits)
    {
      var d = new Date(); 
      var hours = parseInt(bits[1], 10);
      var minutes = 0;
      var ampm = bits[2].toLowerCase();
      return getTimeObj(hours, minutes, ampm);
    }
  }
, { // two digits - 24 hour
    re: /^([1-9]|1[0-9]|2[0-3]):([0-5]\d)$/i
  , handler: function(bits)
    {
      var d = new Date(); 
      var hours = parseInt(bits[1], 10);
      var minutes = parseInt(bits[2], 10);
      var ampm = 'am';
      if(hours > 12)
      {
        hours = hours - 12;
        ampm = 'pm';
      }
      return getTimeObj(hours, minutes, ampm);
    }
  }
, { // two digits - 24 hour - no minutes
    re: /^([1-9]|1[0-9]|2[0-3])$/i
  , handler: function(bits)
    {
      var d = new Date(); 
      var hours = parseInt(bits[1], 10);
      var minutes = 0;
      var ampm = 'am';
      if(hours > 12)
      {
        hours = hours - 12;
        ampm = 'pm';
      }
      return getTimeObj(hours, minutes, ampm);
    }
  }
];

function parseDateString(s)
{
  for(var i = 0; i < dateParsePatterns.length; i++)
  {
    var re = dateParsePatterns[i].re;
    var handler = dateParsePatterns[i].handler;
    var bits = re.exec(s);
    if (bits)
      return handler(bits);
  }
  throw new Error('Invalid date string');
}

function parseTimeString(s)
{
  for(var i = 0; i < timeParsePatterns.length; i++)
  {
    var re = timeParsePatterns[i].re;
    var handler = timeParsePatterns[i].handler;
    var bits = re.exec(s);
    if (bits)
      return handler(bits);
  }
  throw new Error('Invalid time string');
}

function magicDate(input)
{
  try
  {
    var d = parseDateString(input.value.trim());
    switch (configDateType)
    {
      case 'us':
        input.value = ((d.getMonth() + 1 < 10) ? '0' : '') + (d.getMonth() + 1) + '/' + ((d.getDate() < 10) ? '0' : '') + d.getDate() + '/' + d.getFullYear();
        break;
      case 'iso':
      default:
        input.value = d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate();
        break;
    }
  }
  catch (e) { }
}

function magicTime(input)
{
  try
  {
    var t = parseTimeString(input.value.trim());
    switch (configTimeType)
    {
      case 24:
        input.value = ((t.getHours() < 10) ? '0' : '') + t.getHours() + ':' + ((t.getMinutes() < 10) ? '0' : '') + t.getMinutes();
        break;
      case 12:
      default:
      {
        hours = t.getHours();
        ampm = (hours <= 12) ? ((hours >= 7 && hours != 12) ? 'am' : 'pm') : 'pm'
        hours = ((hours > 12) ? hours - 12 : hours);
        input.value = ((hours < 10) ? '0' : '') + hours + ':' + ((t.getMinutes() < 10) ? '0' : '') + t.getMinutes() + ' ' + ampm;
        break;
      }
    }
  }
  catch (e) { }
};

function updateHidden(oHidden, sDate, sTime)
{
  var datetime = new Date();
  oHidden.value = ((sDate != null) ? (datetime.getMonth() + 1) + '/' + (datetime.getDate()) + '/' + (datetime.getFullYear()) : '')
                + ' ' 
                + ((sTime != null) ? (datetime.getHours() + 1) + ':' + datetime.getMinutes() : '');
}
