/*  protocalendar.js
 *  (c) 2007 Spookies
 * 
 *  License : MIT-style license.
 *  Web site: http://labs.spookies.co.jp
 *
 *  protocalendar.js - depends on prototype.js
 *  http://www.prototypejs.org/
 *
/*--------------------------------------------------------------------------*/

var ProtoCalendar = Class.create();
ProtoCalendar.Version = "1.0";

ProtoCalendar.LangFile = new Object();
ProtoCalendar.LangFile['en'] = {
  DEFAULT_FORMAT: 'mm/dd/yyyy',
  LABEL_FORMAT: 'ddd mm/dd/yyyy',
  MONTH_ABBRS: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'],
  MONTH_NAMES: ['January','February','March','April','May','June','July','August','September','October','November','December'],
  YEAR_LABEL: ' ',
  MONTH_LABEL: ' ',
  WEEKDAY_ABBRS: ['Sun','Mon','Tue','Wed','Thr','Fri','Sat'],
  WEEKDAY_NAMES: ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'],
  YEAR_AND_MONTH: false
};

ProtoCalendar.LangFile.defaultLang = 'en';
ProtoCalendar.LangFile.defaultLangFile = function() { return ProtoCalendar.LangFile[defaultLang]; };

Object.extend(ProtoCalendar, {
                JAN: 0,
                FEB: 1,
                MAR: 2,
                APR: 3,
                MAY: 4,
                JUNE: 5,
                JULY: 6,
                AUG: 7,
                SEPT: 8,
                OCT: 9,
                NOV: 10,
                DEC: 11,

                SUNDAY: 0,
                MONDAY: 1,
                TUESDAY: 2,
                WEDNESDAY: 3,
                THURSDAY: 4,
                FRIDAY: 5,
                SATURDAY: 6,

                getNumDayOfMonth: function(year, month){
                  return 32 - new Date(year, month, 32).getDate();
                },

                getDayOfWeek: function(year, month, day) {
                  return new Date(year, month, day).getDay();
                }
              });

ProtoCalendar.prototype = {
  initialize: function(options) {
    var date = new Date();
    this.options = Object.extend({
                                   month: date.getMonth(),
                                   year: date.getFullYear(),
                                   lang: ProtoCalendar.LangFile.defaultLang
                                 }, options || { });
    var getHolidays = ProtoCalendar.LangFile[this.options.lang]['getHolidays'];
    if (getHolidays) {
      this.initializeHolidays = getHolidays.bind(top, this);
    } else {
      this.initializeHolidays = function() { this.holidays = []; };
    }
    this.date = new Date(this.options.year, this.options.month, 1);
  },

  getMonth: function() {
    return this.date.getMonth();
  },

  getYear: function() {
    return this.date.getFullYear();
  },

  invalidate: function() {
    this.holidays = undefined;
  },

  setMonth: function(month) {
    if (month != this.getMonth()) {
      this.invalidate();
    }
    return this.date.setMonth(month);
  },

  setYear: function(year) {
    if (year != this.getYear()) {
      this.invalidate();
    }
    return this.date.setFullYear(year);
  },

  getDate: function() {
    return this.date;
  },

  setDate: function(date) {
    this.invalidate();
    this.date = date;
  },

  setYearByOffset: function(offset) {
    if (offset != 0) {
      this.invalidate();
    }
    this.date.setFullYear(this.date.getFullYear() + offset);
  },

  setMonthByOffset: function(offset) {
    if (offset != 0) {
      this.invalidate();
    }
    this.date.setMonth(this.date.getMonth() + offset);
  },

  getNumDayOfMonth: function() {
    return ProtoCalendar.getNumDayOfMonth(this.getYear(), this.getMonth());
  },

  getDayOfWeek: function(day) {
    return ProtoCalendar.getDayOfWeek(this.getYear(), this.getMonth(), day);
  },

  clone: function() {
    return new ProtoCalendar({year: this.getYear(), month: this.getMonth()});
  },

  getHoliday: function(day) {
    if(!this.holidays) { this.initializeHolidays();}
    var holiday = this.holidays[day];
    return holiday? holiday : false;
  },

  initializeHolidays: function() {
  }
};

var AbstractProtoCalendarRender = Class.create();
Object.extend(AbstractProtoCalendarRender, {
                id: 1,
                WEEK_DAYS_SUNDAY: [ 0, 1, 2, 3, 4, 5, 6 ],
                WEEK_DAYS_MONDAY: [ 1, 2, 3, 4, 5, 6, 0 ],
                WEEK_DAYS_INDEX_SUNDAY: [ 0, 1, 2, 3, 4, 5, 6 ],
                WEEK_DAYS_INDEX_MONDAY: [ 6, 0, 1, 2, 3, 4, 5 ],

                getId: function() {
                  var id = AbstractProtoCalendarRender.id;
                  AbstractProtoCalendarRender.id += 1;
                  return id;
                }
               });

AbstractProtoCalendarRender.prototype = {
  initialize: function(options) {
    this.id = AbstractProtoCalendarRender.getId();
    this.options = Object.extend({
                                   weekFirstDay : ProtoCalendar.MONDAY,
                                   yearSpan: 10,
                                   containerClass: 'cal-container',
                                   tableClass: 'cal-table',
                                   headerClass: 'cal-header',
                                   bodyClass: 'cal-body',
                                   footerClass: 'cal-footer',
                                   selectYearClass: 'cal-select-year',
                                   selectYearId: this.getIdPrefix() + "-select-year",
                                   selectMonthClass: 'cal-select-month',
                                   selectMonthId: this.getIdPrefix() + "-select-month",
                                   labelRowClass: 'cal-label-row',
                                   labelCellClass: 'cal-label-cell',
                                   nextButtonClass: 'cal-next-btn',
                                   prevButtonClass: 'cal-prev-btn',
                                   dayCellClass: 'cal-day-cell',
                                   dayClass: 'cal-day',
                                   weekdayClass: 'cal-weekday',
                                   sundayClass: 'cal-sunday',
                                   saturdayClass: 'cal-saturday',
                                   holidayClass: 'cal-holiday',
                                   otherdayClass: 'cal-otherday',
                                   selectedDayClass: 'cal-selected',
                                   nextBtnId: this.getIdPrefix() + "-next-btn",
                                   prevBtnId: this.getIdPrefix() + "-prev-btn",
                                   lang: ProtoCalendar.LangFile.defaultLang
                                 }, options || {});
    this.langFile = ProtoCalendar.LangFile[this.options.lang];
    this.weekFirstDay = this.options.weekFirstDay;
    this.initWeekData();
    this.container = this.createContainer();
    this.alignTo = $(this.options.alignTo);
    if (navigator.appVersion.match(/\bMSIE\b/)) {
      this.iframe = this.createIframe();
    }
  },

  createContainer: function() {
    var container = $(document.createElement('div'));
    container.addClassName(this.options.containerClass);
    container.setStyle({position:'absolute',
                        top: "0px",
                        left: "0px",
                        zindex:1,
                        display: 'none'});
    container.hide();
    document.body.appendChild(container);
    return container;
  },

  createIframe: function() {
    var iframe = document.createElement("iframe");
    iframe.setAttribute("src", "javascript:false;");
    iframe.setAttribute("frameBorder", "0");
    iframe.setAttribute("scrolling", "no");
    Element.setStyle(iframe, { position:'absolute',
                               top: "0px",
                               left: "0px",
                               zindex:10,
                               display: 'none',
                               overflow: 'hidden',
                               filter: 'progid:DXImageTransform.Microsoft.Alpha(opacity=0)'
                             });
    document.body.appendChild(iframe);
    return $(iframe);
  },

  getWeekdayLabel: function(weekday) {
    return this.langFile.WEEKDAY_ABBRS[weekday];
  },

  getWeekdays: function() {
    return this.weekdays;
  },

  initWeekData: function() {
    if (this.weekFirstDay == ProtoCalendar.SUNDAY) {
      this.weekLastDay = ProtoCalendar.SATURDAY;
      this.weekdays = AbstractProtoCalendarRender.WEEK_DAYS_SUNDAY;
      this.weekdaysIndex = AbstractProtoCalendarRender.WEEK_DAYS_INDEX_SUNDAY;
    } else {
      this.weekFirstDay == ProtoCalendar.MONDAY
      this.weekLastDay = ProtoCalendar.SUNDAY;
      this.weekdays = AbstractProtoCalendarRender.WEEK_DAYS_MONDAY;
      this.weekdaysIndex = AbstractProtoCalendarRender.WEEK_DAYS_INDEX_MONDAY;
    }
  },

  getProtoCalendarBeginDay: function(calendar) {
    var offset = this.getDayIndexOfWeek(calendar, 1);
    var date = new Date(calendar.getYear(), calendar.getMonth(), 1 - offset);
    return date;
  },

  getProtoCalendarEndDay: function(calendar) {
    var lastDayOfMonth = calendar.getNumDayOfMonth();
    var offset = 6 - this.getDayIndexOfWeek(calendar, lastDayOfMonth);
    var date = new Date(calendar.getYear(), calendar.getMonth(), lastDayOfMonth + offset + 1);
    return date;
  },

  getDayIndexOfWeek: function(calendar, day) {
    return this.weekdaysIndex[ calendar.getDayOfWeek(day) ];
  },

  getIdPrefix: function() {
    return 'cal' + this.id;
  },

  getDayDivId: function(date) {
    return this.getIdPrefix() + '-month' + date.getMonth() + '-day' + date.getDate();
  },

  setPosition: function() {
    if (!this.alignTo) return;
    setAlignment(this.alignTo, this.container);
    if (this.iframe) {
      var dimensions = Element.getDimensions(this.container);
      this.iframe.setAttribute("width", dimensions.width);
      this.iframe.setAttribute("height", dimensions.height);
      setAlignment(this.alignTo, this.iframe);
    }
  },

  show: function() {
   this.setPosition();
    if (typeof Effect != 'undefined') {
      new Effect.Appear(this.container, {duration: 0.5});
    } else {
      this.container.show();
    }
    if (this.iframe) this.iframe.show();
  },

  hide: function() {
    this.container.hide();
    if (this.iframe) this.iframe.hide();
  },

  toggle: function(element) {
    this.container.visible() ? this.hide() : this.show();
  },

  render: function(calendar) { },

  getContainer: function() {
    return this.container;
  },

  getPrevButton: function() {
    return $(this.options.prevBtnId);
  },

  getNextButton: function() {
    return $(this.options.nextBtnId);
  },

  getSelectYear: function() {
    return $(this.options.selectYearId);
  },

  getSelectMonth: function() {
    return $(this.options.selectMonthId);
  },

  getDayDivs: function() {
    return this.container.getElementsBySelector("a." + this.options.dayClass);
  }

};

var ProtoCalendarRender = Class.create();
Object.extend(ProtoCalendarRender.prototype, AbstractProtoCalendarRender.prototype);

Object.extend(
  ProtoCalendarRender.prototype,
  {
    render: function(calendar) {
      var html = '';
      html += this.renderHeader(calendar);
      html += '<div class="#{bodyClass}"><table class="#{tableClass}" cellspacing="0">';
      html += this.renderBody(calendar);
      html += '</table></div>';
      html += this.renderFooter(calendar);
      var template = new Template(html);
      this.container.innerHTML = template.evaluate(this.options);
      this.getSelectMonth().selectedIndex = calendar.getMonth();
    },

    renderHeader: function(calendar) {
      var html = '';
      // required 'href'
      html += '<div class="#{headerClass}">' +
        '<a href="javascript:void(0);" id="#{prevBtnId}" class="#{prevButtonClass}">&lt;&lt;</a>' +
        this.createSelect(calendar.getYear(), calendar.getMonth()) +
        '<a href="javascript:void(0);" id="#{nextBtnId}" class="#{nextButtonClass}">&gt;&gt;</a>' +
        '</div>';
      return html;
    },

    renderFooter: function(calendar) {
      return '<div class="#{footerClass}"></div>';
    },

    createSelect: function(year, month) {
      var yearPart = this.createYearSelect(year) + this.langFile.YEAR_LABEL;
      var monthPart = this.createMonthSelect(month) + this.langFile.MONTH_LABEL;
      if (this.langFile.YEAR_AND_MONTH) {
        return  yearPart + monthPart;
      } else {
        return monthPart + yearPart;
      }
    },

    createYearSelect: function(year) {
      var html = '';
      html += '<select id="#{selectYearId}" class="#{selectYearClass}">';
      var span = this.options.yearSpan;
      for (var y = year - span, i = 0; y <= year + span; y += 1, i += 1) {
        html += '<option value="' + y + '"' + (y == year ? ' selected' : '') + '>' + y + '</option>';
      }
      html += '</select>';
      return html;
    },

    createMonthSelect: function(month) {
      if (!this.monthSelectHtml) {
        var html = '';
        html += '<select id="#{selectMonthId}" class="#{selectMonthClass}">';
        for (var m = ProtoCalendar.JAN; m <= ProtoCalendar.DEC; m += 1) {
          html += '<option value="' + m + '"' + (m == month ? ' selected' : '') + '>' + this.langFile['MONTH_ABBRS'][m] + '</option>';
        }
        html += '</select>';
        this.monthSelectHtml = html;
      }
      return this.monthSelectHtml;
    },

    renderBody: function(calendar) {
      var html = '';
      html += '<tr class="#{labelRowClass}">';
      var othis = this;
      if (!this.headHtml) {
        this.headHtml = '';
        $A(this.getWeekdays()).each(function(weekday) {
                                      var exClassName = '';
                                      if (weekday == ProtoCalendar.SUNDAY) { exClassName = ' #{sundayClass}'; }
                                      if (weekday == ProtoCalendar.SATURDAY) { exClassName = ' #{saturdayClass}'; }
                                      othis.headHtml += '<th class="#{labelCellClass}' + exClassName + '">' +
                                        othis.getWeekdayLabel(weekday) +
                                        '</th>';
                                    });
      }
      html += this.headHtml;
      var curDay = this.getProtoCalendarBeginDay(calendar);
      var calEndDay = this.getProtoCalendarEndDay(calendar);
      html += '<tbody>';
      var dayNum = Math.round((calEndDay - curDay) / 1000 / 60 / 60 / 24);
      for(var i = 0; i < dayNum; i += 1, curDay.setDate(curDay.getDate() + 1)) {
        var divClassName;
        var holiday = calendar.getHoliday(curDay.getDate());
        if(curDay.getMonth() != calendar.getMonth()) {
          divClassName = this.options.otherdayClass;
        } else if (holiday) {
          divClassName = this.options.holidayClass;
        } else if (curDay.getDay() == ProtoCalendar.SUNDAY) {
          divClassName = this.options.sundayClass;
        } else if (curDay.getDay() == ProtoCalendar.SATURDAY) {
          divClassName = this.options.saturdayClass;
        } else {
          divClassName = this.options.weekdayClass;
        }

        if (curDay.getDay() == this.weekFirstDay) { html += '<tr>'; }
        html += '<td class="' + divClassName + ' #{dayCellClass}">' +
          '<a class="#{dayClass}" href="javascript:void(0)" id="' + this.getDayDivId(curDay) +
          (holiday ? '" title="' + holiday : '') +
          '" year="' + curDay.getFullYear() +
          '" month="' + curDay.getMonth() +
          '" day="' + curDay.getDate() +
          '">' + curDay.getDate() + '</a>';

        if (curDay.getDay() == this.weekLastDay) { html += '</tr>'; }
      }
      html += '</tbody>';
      return html;
    },

    getDayDivs: function() {
      return this.container.getElementsBySelector("a." + this.options.dayClass);
    }
  });


var ProtoCalendarController = Class.create();

//Object.extend(ProtoCalendarController, { });

ProtoCalendarController.prototype = {
  initialize: function(calendarRender, options) {
    var today = new Date();
    this.options = Object.extend({ year : today.getFullYear(),
                                   month : today.getMonth(),
                                   day : today.getDate()
                                 }, options || {});
    this.calendarRender = calendarRender;
    this.calendar = new ProtoCalendar(options);
    this.calendarRender.render(this.calendar);
    this.selectDate(new Date(this.options.year, this.options.month, this.options.day));
    this.observeEvents();
    this.onChangeHandlers = [];
  },

  observeEvents: function() {
    var calrndr = this.calendarRender;
    calrndr.getPrevButton().observe('click', this.showPrevMonth.bindAsEventListener(this));
    calrndr.getNextButton().observe('click', this.showNextMonth.bindAsEventListener(this));
    var othis = this;
    var selectYear = calrndr.getSelectYear();
    var selectMonth = calrndr.getSelectMonth();
    var year = this.calendar.getYear();
    var month = this.calendar.getMonth();
    selectYear.observe('change', function() {
                         othis.setMonth(parseInt(selectYear[selectYear.selectedIndex].value), month);
                       });
    selectMonth.observe('change', function() {
                          othis.setMonth(year, parseInt(selectMonth[selectMonth.selectedIndex].value));
                       });
    var handler = this.onClickHandler;
    calrndr.getDayDivs().each(function(el) {
                                            el.observe('click', handler.bindAsEventListener(othis));
                                          });
  },

  onClickHandler: function(event) {
    this.selectDate(this.__getDateFromEl(Event.element(event)));
    this.onChangeHandler();
    this.hideProtoCalendar();
  },

  selectDate: function(date) {
    if (this.selectedDate) {
      var dateEl = $(this.calendarRender.getDayDivId(this.selectedDate));
      if (dateEl) dateEl.removeClassName(this.calendarRender.options.selectedDayClass);
    }
    this.selectedDate = date;
    if (!date) return;
//     if (date.getFullYear() != this.calendar.getYear() || date.getMonth() != this.calendar.getMonth()) {
//       this.setMonth(date.getFullYear(), date.getMonth());
//     }
    var dayEl = $(this.calendarRender.getDayDivId(date));
    if (dayEl) dayEl.addClassName(this.calendarRender.options.selectedDayClass);
  },

  __getDateFromEl: function(el) {
    var element = $(el);
    return new Date(element.readAttribute('year'), element.readAttribute('month'), element.readAttribute('day'));
  },

  getSelectedDate: function() {
    return this.selectedDate;
  },

  addChangeHandler: function(func) {
    this.onChangeHandlers.push(func);
  },

  onChangeHandler: function() {
    this.onChangeHandlers.each(function(f) { f(); });
  },

  showProtoCalendar: function() {
    this.calendarRender.show();
  },

  hideProtoCalendar: function() {
    this.calendarRender.hide();
  },

  toggleProtoCalendar: function() {
    this.calendarRender.toggle();
  },

  showPrevMonth: function(event) {
    this.shiftMonthByOffset(-1);
    if (event) Event.stop(event);
  },

  showNextMonth: function(event) {
    this.shiftMonthByOffset(1);
    if (event) Event.stop(event);
  },

  shiftMonthByOffset: function(offset) {
    this.calendar.setMonthByOffset(offset);
    this.afterSet();
  },

  setMonth: function(year, month) {
    this.calendar.setYear(year);
    this.calendar.setMonth(month);
    this.afterSet();
  },

  afterSet: function() {
    this.calendarRender.render(this.calendar);
    this.selectDate(this.selectedDate);
    this.observeEvents();
  },

  getContainer: function() {
    return this.calendarRender.getContainer();
  }
};

var InputCalendar = Class.create();
InputCalendar.prototype = {
  initialize: function(input, options) {
    this.input = $(input);
    if (!options) options = {};
    this.options = Object.extend({
                                   format: ProtoCalendar.LangFile[options.lang || ProtoCalendar.LangFile.defaultLang]['DEFAULT_FORMAT'],
                                   lang: ProtoCalendar.LangFile.defaultLang,
                                   inputReadOnly: false,
                                   alignTo: input,
                                   labelFormat: undefined,
                                   labelEl: undefined
                                 }, options || { });
    this.calendarController = new ProtoCalendarController(new ProtoCalendarRender(this.options), this.options);
    this.dateFormat = new DateFormat(this.options.format);
    this.langFile = ProtoCalendar.LangFile[this.options.lang] || ProtoCalendar.LangFile.defaultLangFile();
    if (this.input.value && this.dateFormat.parse(this.input.value)) {
      this.onInputChange();
    } else {
      this.onProtoCalendarChange();
    }
    if (this.options.inputReadOnly) this.input.setAttribute('readOnly', this.options.inputReadOnly);
    this.observeEvents();
    this.triggers = [];
    this.labelFormat = new DateFormat(this.options.labelFormat || this.langFile['LABEL_FORMAT']);
    var labelElm = $(this.options.labelEl);
    if ((! labelElm) && this.options.labelFormat) {
      var labelId = this.input.id + '_label';
      new Insertion.After(this.input, "<div id='" + labelId + "'></div>");
      labelElm = $(labelId);
    }
    this.labelEl = labelElm;
    this.changeLabel();
  },

  addTrigger: function(el) {
    this.triggers.push($(el));
    $(el).setStyle({'cursor': 'pointer'});
  },

  observeEvents: function() {
    this.input.observe('change', this.onInputChange.bind(this));
    this.input.observe('focus', this.calendarController.showProtoCalendar.bind(this.calendarController));
    Event.observe(document, 'click', this.windowClickHandler.bindAsEventListener(this));
    this.calendarController.addChangeHandler(this.onProtoCalendarChange.bind(this));
  },

  windowClickHandler: function(event) {
    var target = $(Event.element(event));
    if (this.triggers.include(target)) {
      this.calendarController.toggleProtoCalendar();
    } else if (target != this.input && !Element.descendantOf(target, this.calendarController.getContainer())) {
      this.calendarController.hideProtoCalendar();
    }
  },

  onInputChange: function() {
    var date = this.dateFormat.parse(this.input.value);
    if (date) {
      this.calendarController.selectDate(date);
    } else {
      var inputValue = this.input.value.toLowerCase();
      var date;
      if (this.langFile['today'] && this.langFile['today'] == inputValue || inputValue == 'today') {
        date = new Date();
      } else if (this.langFile['tomorrow'] && this.langFile['tomorrow'] == inputValue || inputValue == 'tomorrow') {
        date = new Date();
        date.setDate(date.getDate() + 1);
      } else if (this.langFile['yesterday'] && this.langFile['yesterday'] == inputValue || inputValue == 'yesterday') {
        date = new Date();
        date.setDate(date.getDate() - 1);
      } else if (this.langFile.parseDate && (date = this.langFile.parseDate(inputValue))) {
        //done is parseDate
      } else {
        date = undefined;
      }
      this.calendarController.selectDate(date);
      this.onProtoCalendarChange();
    }
    this.changeLabel();
  },

  onProtoCalendarChange: function() {
    this.input.value = this.dateFormat.format(this.calendarController.getSelectedDate(), this.options.lang);
    this.changeLabel();
  },

  changeLabel: function() {
    if (!this.labelEl) return;
    if (!this.calendarController.getSelectedDate()) {
      this.labelEl.innerHTML = this.labelFormat.dateFormat;
    } else {
      this.labelEl.innerHTML = this.labelFormat.format(this.calendarController.getSelectedDate(), this.options.lang);
    }
  }
};

function setAlignment(alignTo, element) {
  var offsets = Position.cumulativeOffset(alignTo);
  element.setStyle({left: offsets[0] + "px", top: (offsets[1] + alignTo.offsetHeight) + "px"});
}


var DateFormat = Class.create();

Object.extend(DateFormat,
              {
                MONTH_ABBRS: ProtoCalendar.LangFile.en.MONTH_ABBRS,
                MONTH_NAMES: ProtoCalendar.LangFile.en.MONTH_NAMES,
                WEEKDAY_ABBRS: ProtoCalendar.LangFile.en.WEEKDAY_ABBRS,
                WEEKDAY_NAMES: ProtoCalendar.LangFile.en.WEEKDAY_NAMES,
                formatRegexp: /\b(?:d{1,4}|d{3,4}i|m{1,4}|yy(?:yy)?|([hHMs])\1?|TT|tt|[lL])\b|.+?/g,
                zeroize: function (value, length) {
                  if (!length) length = 2;
                  value = String(value);
                  for (var i = 0, zeros = ''; i < (length - value.length); i++) {
                    zeros += '0';
                  }
                  return zeros + value;
                }
              });

DateFormat.prototype =  {
  initialize: function(format) {
    this.dateFormat = format;
    this.parserInited = false;
    this.formatterInited = false;
  },

  format: function(date, lang) {
    if (!this.formatterInited) this.initFormatter();
    if (!date) return '';
    var langFile = ProtoCalendar.LangFile[lang || ProtoCalendar.LangFile.defaultLang];
    var str = '';
    this.formatHandlers.each(function(f) {
                               str += f(date, langFile);
                             });
    return str;
  },

  initFormatter: function() {
    var handlers = [];
    var matches = this.dateFormat.match(DateFormat.formatRegexp);
    for (var i = 0, n = matches.length; i < n; i++) {
      switch(matches[i]) {
      case 'd':       handlers.push(function(date, lf) { return date.getDate(); }); break;
      case 'dd':      handlers.push(function(date, lf) { return DateFormat.zeroize(date.getDate()) }); break;
      case 'ddd':     handlers.push(function(date, lf) { return DateFormat.WEEKDAY_ABBRS[date.getDay()]; }); break;
      case 'dddd':    handlers.push(function(date, lf) { return DateFormat.WEEKDAY_NAMES[date.getDay()]; }); break;
      case 'dddi':    handlers.push(function(date, lf) { return lf.WEEKDAY_ABBRS[date.getDay()]; }); break;
      case 'ddddi':   handlers.push(function(date, lf) { return lf.WEEKDAY_NAMES[date.getDay()]; }); break;
      case 'm':       handlers.push(function(date, lf) { return date.getMonth() + 1; }); break;
      case 'mm':      handlers.push(function(date, lf) { return DateFormat.zeroize(date.getMonth() + 1); }); break;
      case 'mmm':     handlers.push(function(date, lf) { return lf.MONTH_ABBRS[date.getMonth()]; }); break;
      case 'mmmm':    handlers.push(function(date, lf) { return (lf.MONTH_NAMES || DateFormat)[date.getMonth()]; }); break;
      case 'yy':      handlers.push(function(date, lf) { return String(date.getFullYear()).substr(2); }); break;
      case 'yyyy':    handlers.push(function(date, lf) { return date.getFullYear(); }); break;
      case 'h':       handlers.push(function(date, lf) { return date.getHours() % 12 || 12; }); break;
      case 'hh':      handlers.push(function(date, lf) { return DateFormat.zeroize(date.getHours() % 12 || 12); }); break;
      case 'H':       handlers.push(function(date, lf) { return date.getHours(); }); break;
      case 'HH':      handlers.push(function(date, lf) { return DateFormat.zeroize(date.getHours()); }); break;
      case 'M':       handlers.push(function(date, lf) { return date.getMinutes(); }); break;
      case 'MM':      handlers.push(function(date, lf) { return DateFormat.zeroize(date.getMinutes()); }); break;
      case 's':       handlers.push(function(date, lf) { return date.getSeconds(); }); break;
      case 'ss':      handlers.push(function(date, lf) { return DateFormat.zeroize(date.getSeconds()); }); break;
      case 'l':       handlers.push(function(date, lf) { return DateFormat.zeroize(date.getMilliseconds(), 3); }); break;
      case 'tt':      handlers.push(function(date, lf) { return date.getHours() < 12 ? 'am' : 'pm'; }); break;
      case 'TT':      handlers.push(function(date, lf) { return date.getHours() < 12 ? 'AM' : 'PM'; }); break;
      default:        handlers.push(createIdentity(matches[i]));
      }
    };
    this.formatHandlers = handlers;
    this.formatterInited = true;
  },

  parse: function(str) {
    if (!this.parserInited) this.initParser();
    if (!str) return undefined;
    var results = str.match(this.parserRegexp);
    if (!results) return undefined;
    var date = new Date();
    var handler;
    for (var i = 0, n = this.parseHandlers.length; i < n; i++) {
      if (this.parseHandlers[i] != undefined) {
        (this.parseHandlers[i])(date, results[i+1]);
      }
    }
    this.parseCallback(date);
    return date;
  },

  initParser: function() {
    var handlers = [];
    var regstr = '';
    var matches = this.dateFormat.match(DateFormat.formatRegexp);
    var hour, ampm;

    for (var i = 0, n = matches.length; i < n; i++) {
      regstr += '(';
      switch(matches[i]) {
      case 'd':
      case 'dd':      regstr += '\\d{1,2}';
                      handlers.push(function(date, value) { date.setDate(value); });
                      break;
      case 'm':
      case 'mm':      regstr += '\\d{1,2}';
                      handlers.push(function(date, value) { date.setMonth(parseInt(value) - 1);});
                      break;
//       case 'mmm':     regstr += DateFormat.MONTH_ABBRS.join('|');
//                       handlers.push(function(date, value) {
//                                       date.setMonth(DateFormat.MONTH_ABBRS.indexOf(value)); });
//                       break;
//       case 'mmmm':    regstr += DateFormat.MONTH_NAMES.join('|');
//                       handlers.push(function(date, value) {
//                                       date.setMonth(DateFormat.MONTH_NAMES.indexOf(value)); });
//                       break;
      case 'yy':      regstr += '\\d{2}';
                      handlers.push(function(date, value) {
                                      var year = parseInt(value);
                                      year = year < 70 ? 2000 + year : 1900 + year;
                                      date.setFullYear(year); });
                      break;
      case 'yyyy':    regstr += '\\d{4}';
                      handlers.push(function(date, value) { date.setFullYear(value); });
                      break;
      case 'h':
      case 'hh':      hour = true;
                      regstr += '\\d{1,2}';
                      handlers.push(function(date, value) {
                                      value = value % 12 || 0;
                                      date.setHours(value);
                                      });
                      break;
      case 'H':
      case 'HH':      regstr += '\\d{1,2}';
                      handlers.push(function(date, value) { date.setHours(value); });
                      break;
      case 'M':
      case 'MM':      regstr += '\\d{1,2}';
                      handlers.push(function(date, value) { date.setMinutes(value); });
                      break;
      case 's':
      case 'ss':      regstr += '\\d{1,2}';
                      handlers.push(function(date, value) { date.setSeconds(value); });
                      break;
      case 'l':       regstr += '\\d{1,3}';
                      handlers.push(function(date, value) { date.setMilliSeconds(value); });
                      break;
      case 'tt':      regstr += 'am|pm';
                      handlers.push(function(date, value) { ampm = value; });
                      break;
      case 'TT':      regstr += 'AM|PM';
                      handlers.push(function(date, value) { ampm = value.toLowerCase(); });
                      break;
      case 'mmm':
      case 'mmmm':
      case 'ddd':
      case 'dddd':
      case 'dddi':
      case 'ddddi':   regstr += '.+?';
                      handlers.push(undefined);
                      break;

      default:        regstr += matches[i];
                      handlers.push(undefined);
      }
      regstr += ')';
    }
    this.parserRegexp = new RegExp(regstr);
    this.parseHandlers = handlers;

    if (ampm == 'pm' && hour) {
      this.parseCallback = this.normalizeHour.bind(this);
    } else {
      this.parseCallback = function() {};
    }
    this.parserInited = true;
  },

  normalizeHour: function(date) {
    var hour = date.getHours();
    hour = hour == 12 ? 0 : hour + 12;
    date.setHours(hour);
  }
};

function createIdentity(v) {
  return function() { return v; }
}
