// Calendar: a Javascript class for Mootools that adds accessible and unobtrusive date pickers to your form elements <http://electricprism.com/aeron/calendar>
// Calendar RC4, Copyright (c) 2007 Aeron Glemann <http://electricprism.com/aeron>, MIT Style License.

var Calendar = new Class({       

       options: {
              blocked: [], // blocked dates 
              classes: [], // ['calendar', 'prev', 'next', 'month', 'year', 'today', 'invalid', 'valid', 'inactive', 'active', 'hover', 'hilite']
              days: ['Niedziela', 'Poniedziałek', 'Wtorek', 'Środa', 'Czwartek', 'Piątek', 'Sobota'], // days of the week starting at sunday
              direction: 0, // -1 past, 0 past + future, 1 future
              draggable: true,
              months: ['Styczeń', 'Luty', 'Marzec', 'Kwiecień', 'Maj', 'Czerwiec', 'Lipiec', 'Sierpień', 'Wrzesień', 'Październik', 'Listopad', 'Grudzień'],
              navigation: 1, // 0 = no nav; 1 = single nav for month; 2 = dual nav for month and year
              offset: 0, // first day of the week: 0 = sunday, 1 = monday, etc..
              onHideStart: Class.empty,
              onHideComplete: Class.empty,
              onShowStart: Class.empty,
              onShowComplete: Class.empty,
              pad: 1, // padding between multiple calendars
              tweak: {x: 0, y: 0} // tweak calendar positioning
       },

       // initialize: calendar constructor
       // @param obj (obj) a js object containing the form elements and format strings { id: 'format', id: 'format' etc }
       // @param props (obj) optional properties

       initialize: function(obj, options) {
              // basic error checking
              if (!obj) { return false; }

              this.setOptions(options);

              // create our classes array
              var keys = ['calendar', 'prev', 'next', 'month', 'year', 'today', 'invalid', 'valid', 'inactive', 'active', 'hover', 'hilite'];

              var values = keys.map(function(key, i) {
                     if (this.options.classes[i]) {
                            if (this.options.classes[i].length) { key = this.options.classes[i]; }
                     }
                     return key;
              }, this);

              this.classes = values.associate(keys);

              // create cal element with css styles required for proper cal functioning
              this.calendar = new Element('div', { 
                     'styles': { left: '-1000px', opacity: 0, position: 'absolute', top: '-1000px', zIndex: 1000 }
              }).addClass(this.classes.calendar).injectInside(document.body);

              // iex 6 needs a transparent iframe underneath the calendar in order to not allow select elements to render through
              if (window.ie6) {
                     this.iframe = new Element('iframe', { 
                            'styles': { left: '-1000px', position: 'absolute', top: '-1000px', zIndex: 999 }
                     }).injectInside(document.body);
                     this.iframe.style.filter = 'progid:DXImageTransform.Microsoft.Alpha(style=0,opacity=0)';
              }

              // initialize fade method
              this.fx = this.calendar.effect('opacity', { 
                     onStart: function() { 
                            if (this.calendar.getStyle('opacity') == 0) { // show
                                   if (window.ie6) { this.iframe.setStyle('display', 'block'); }
                                   this.calendar.setStyle('display', 'block');
                                   this.fireEvent('onShowStart', this.element);
                            }
                            else { // hide
                                   this.fireEvent('onHideStart', this.element);
                            }
                     }.bind(this),
                     onComplete: function() { 
                            if (this.calendar.getStyle('opacity') == 0) { // hidden
                                   this.calendar.setStyle('display', 'none');
                                   if (window.ie6) { this.iframe.setStyle('display', 'none'); }
                                   this.fireEvent('onHideComplete', this.element);
                            }
                            else { // shown
                                   this.fireEvent('onShowComplete', this.element);
                            }
                     }.bind(this)
              });

              // initialize drag method
              if (window.Drag && this.options.draggable) {
                     this.drag = new Drag.Move(this.calendar, { 
                            onDrag: function() {
                                   if (window.ie6) { this.iframe.setStyles({ left: this.calendar.style.left, top: this.calendar.style.top }); } 
                            }.bind(this) 
                     }); 
              }
              
              // create calendars array
              this.calendars = [];

              var id = 0;
              var d = new Date(); // today

              d.setDate(d.getDate() + this.options.direction.toInt()); // correct today for directional offset

              for (var i in obj) {
                     var cal = { 
                            button: new Element('button', { 'type': 'button' }),
                            el: $(i),
                            els: [],
                            id: id++,
                            month: d.getMonth(),
                            visible: false,
                            year: d.getFullYear()
                     };

                     // fix for bad element (naughty, naughty element!)
                     if (!this.element(i, obj[i], cal)) { continue; }
                     
                     cal.el.addClass(this.classes.calendar);

                     // create cal button
                     cal.button.addClass(this.classes.calendar).addEvent('click', function(cal) { this.toggle(cal); }.pass(cal, this)).injectAfter(cal.el);

                     // read in default value
                     cal.val = this.read(cal);

                     $extend(cal, this.bounds(cal)); // abs bounds of calendar

                     $extend(cal, this.values(cal)); // valid days, months, years

                     this.rebuild(cal);

                     this.calendars.push(cal); // add to cals array              
              }       
       },


       // blocked: returns an array of blocked days for the month / year
       // @param cal (obj)
       // @returns blocked days (array)

       blocked: function(cal) {
              var blocked = [];
              var offset = new Date(cal.year, cal.month, 1).getDay(); // day of the week (offset)
              var last = new Date(cal.year, cal.month + 1, 0).getDate(); // last day of this month
              
              this.options.blocked.each(function(date){
                     var values = date.split(' ');
                     
                     // preparation
                     for (var i = 0; i <= 3; i++){ 
                            if (!values[i]){ values[i] = (i == 3) ? '' : '*'; } // make sure blocked date contains values for at least d, m and y
                            values[i] = values[i].contains(',') ? values[i].split(',') : new Array(values[i]); // split multiple values
                            var count = values[i].length - 1;
                            for (var j = count; j >= 0; j--){
                                   if (values[i][j].contains('-')){ // a range
                                          var val = values[i][j].split('-');
                                          for (var k = val[0]; k <= val[1]; k++){
                                                 if (!values[i].contains(k)){ values[i].push(k + ''); }
                                          }
                                          values[i].splice(j, 1);
                                   }
                            }
                     }

                     // execution
                     if (values[2].contains(cal.year + '') || values[2].contains('*')){
                            if (values[1].contains(cal.month + 1 + '') || values[1].contains('*')){
                                   values[0].each(function(val){ // if blocked value indicates this month / year
                                          if (val > 0){ blocked.push(val.toInt()); } // add date to blocked array
                                   });

                                   if (values[3]){ // optional value for day of week
                                          for (var i = 0; i < last; i++){
                                                        var day = (i + offset) % 7;
       
                                                        if (values[3].contains(day + '')){ 
                                                               blocked.push(i + 1); // add every date that corresponds to the blocked day of the week to the blocked array
                                                        }
                                          }
                                   }
                            }
                     }
              }, this);

              return blocked;
       },


       // bounds: returns the start / end bounds of the calendar
       // @param cal (obj)
       // @returns obj       

       bounds: function(cal) {
              // 1. first we assume the calendar has no bounds (or a thousand years in either direction)
              
              // by default the calendar will accept a millennium in either direction
              var start = new Date(1000, 0, 1); // jan 1, 1000
              var end = new Date(2999, 11, 31); // dec 31, 2999

              // 2. but if the cal is one directional we adjust accordingly
              var date = new Date().getDate() + this.options.direction.toInt();

              if (this.options.direction > 0) {
                     start = new Date();
                     start.setDate(date + this.options.pad * cal.id);
              }
              
              if (this.options.direction < 0) {
                     end = new Date();
                     end.setDate(date - this.options.pad * (this.calendars.length - cal.id - 1));
              }

              // 3. then we can further filter the limits by using the pre-existing values in the selects
              cal.els.each(function(el) {       
                     if (el.getTag() == 'select') {              
                            if (el.format.test('(y|Y)')) { // search for a year select
                                   var years = [];

                                   el.getChildren().each(function(option) { // get options
                                          var values = this.unformat(option.value, el.format);
       
                                          if (!years.contains(values[0])) { years.push(values[0]); } // add to years array
                                   }, this);
       
                                   years.sort(this.sort);
                     
                                   if (years[0] > start.getFullYear()) { 
                                          d = new Date(years[0], start.getMonth() + 1, 0); // last day of new month
                                   
                                          if (start.getDate() > d.getDate()) { start.setDate(d.getDate()); }
       
                                          start.setYear(years[0]); 
                                   }
                                   
                                   if (years.getLast() < end.getFullYear()) { 
                                          d = new Date(years.getLast(), end.getMonth() + 1, 0); // last day of new month
                                   
                                          if (end.getDate() > d.getDate()) { end.setDate(d.getDate()); }
       
                                          end.setYear(years.getLast());
                                   }              
                            }
       
                            if (el.format.test('(F|m|M|n)')) { // search for a month select
                                   var months_start = [];
                                   var months_end = [];

                                   el.getChildren().each(function(option) { // get options
                                          var values = this.unformat(option.value, el.format);
       
                                          if ($type(values[0]) != 'number' || values[0] == years[0]) { // if it's a year / month combo for curr year, or simply a month select
                                                 if (!months_start.contains(values[1])) { months_start.push(values[1]); } // add to months array
                                          }
       
                                          if ($type(values[0]) != 'number' || values[0] == years.getLast()) { // if it's a year / month combo for curr year, or simply a month select
                                                 if (!months_end.contains(values[1])) { months_end.push(values[1]); } // add to months array
                                          }
                                   }, this);
       
                                   months_start.sort(this.sort);
                                   months_end.sort(this.sort);
                                   
                                   if (months_start[0] > start.getMonth()) { 
                                          d = new Date(start.getFullYear(), months_start[0] + 1, 0); // last day of new month
                                   
                                          if (start.getDate() > d.getDate()) { start.setDate(d.getDate()); }
       
                                          start.setMonth(months_start[0]); 
                                   }
                                   
                                   if (months_end.getLast() < end.getMonth()) { 
                                          d = new Date(start.getFullYear(), months_end.getLast() + 1, 0); // last day of new month
                                   
                                          if (end.getDate() > d.getDate()) { end.setDate(d.getDate()); }
       
                                          end.setMonth(months_end.getLast());
                                   }              
                            }
                     }
              }, this);
              
              return { 'start': start, 'end': end };
       },


       // caption: returns the caption element with header and navigation
       // @param cal (obj)
       // @returns caption (element)

       caption: function(cal) {
              // start by assuming navigation is allowed
              var navigation = {
                     prev: { 'month': true, 'year': true },
                     next: { 'month': true, 'year': true }
              };
              
              // if we're in an out of bounds year
              if (cal.year == cal.start.getFullYear()) { 
                     navigation.prev.year = false; 
                     if (cal.month == cal.start.getMonth() && this.options.navigation == 1) { 
                            navigation.prev.month = false;
                     }              
              }              
              if (cal.year == cal.end.getFullYear()) { 
                     navigation.next.year = false; 
                     if (cal.month == cal.end.getMonth() && this.options.navigation == 1) { 
                            navigation.next.month = false;
                     }
              }

              // special case of improved navigation but months array with only 1 month we can disable all month navigation
              if ($type(cal.months) == 'array') {
                     if (cal.months.length == 1 && this.options.navigation == 2) {
                            navigation.prev.month = navigation.next.month = false;
                     }
              }

              var caption = new Element('caption');

              var prev = new Element('a').addClass(this.classes.prev).appendText('\x3c'); // <              
              var next = new Element('a').addClass(this.classes.next).appendText('\x3e'); // >

              if (this.options.navigation == 2) {
                     var month = new Element('span').addClass(this.classes.month).injectInside(caption);
                     
                     if (navigation.prev.month) { prev.clone().addEvent('click', function(cal) { this.navigate(cal, 'm', -1); }.pass(cal, this)).injectInside(month); }
                     
                     month.adopt(new Element('span').appendText(this.options.months[cal.month]));

                     if (navigation.next.month) { next.clone().addEvent('click', function(cal) { this.navigate(cal, 'm', 1); }.pass(cal, this)).injectInside(month); }

                     var year = new Element('span').addClass(this.classes.year).injectInside(caption);

                     if (navigation.prev.year) { prev.clone().addEvent('click', function(cal) { this.navigate(cal, 'y', -1); }.pass(cal, this)).injectInside(year); }
                     
                     year.adopt(new Element('span').appendText(cal.year));

                     if (navigation.next.year) { next.clone().addEvent('click', function(cal) { this.navigate(cal, 'y', 1); }.pass(cal, this)).injectInside(year); }
              }
              else { // 1 or 0
                     if (navigation.prev.month && this.options.navigation) { prev.clone().addEvent('click', function(cal) { this.navigate(cal, 'm', -1); }.pass(cal, this)).injectInside(caption); }

                     caption.adopt(new Element('span').addClass(this.classes.month).appendText(this.options.months[cal.month]));
                     
                     caption.adopt(new Element('span').addClass(this.classes.year).appendText(cal.year));
                     
                     if (navigation.next.month && this.options.navigation) { next.clone().addEvent('click', function(cal) { this.navigate(cal, 'm', 1); }.pass(cal, this)).injectInside(caption); }

              }

              return caption;
       },


       // changed: run when a select value is changed
       // @param cal (obj)

       changed: function(cal) {
              cal.val = this.read(cal); // update calendar val from inputs       

              $extend(cal, this.values(cal)); // update bounds - based on curr month

              this.rebuild(cal); // rebuild days select

              if (!cal.val) { return; } // in case the same date was clicked the cal has no set date we should exit              

              if (cal.val.getDate() < cal.days[0]) { cal.val.setDate(cal.days[0]); }
              if (cal.val.getDate() > cal.days.getLast()) { cal.val.setDate(cal.days.getLast()); }
              
              cal.els.each(function(el) {       // then we can set the value to the field
                     el.value = this.format(cal.val, el.format);               
              }, this);
              
              this.check(cal); // checks other cals

              this.calendars.each(function(kal) { // update cal graphic if visible
                     if (kal.visible) { this.display(kal); }
              }, this);
       },


       // check: checks other calendars to make sure no overlapping values
       // @param cal (obj)

       check: function(cal) {
              this.calendars.each(function(kal, i) {
                     if (kal.val) { // if calendar has value set
                            var change = false;
                     
                            if (i < cal.id) { // preceding calendar
                                   var bound = new Date(Date.parse(cal.val));
                                   
                                   bound.setDate(bound.getDate() - (this.options.pad * (cal.id - i)));

                                   if (bound < kal.val) { change = true; }
                            }
                            if (i > cal.id) { // following calendar
                                   var bound = new Date(Date.parse(cal.val));
                                   
                                   bound.setDate(bound.getDate() + (this.options.pad * (i - cal.id)));
                                   
                                   if (bound > kal.val) { change = true; }
                            }

                            if (change) {
                                   if (kal.start > bound) { bound = kal.start; }
                                   if (kal.end < bound) { bound = kal.end; }

                                   kal.month = bound.getMonth();
                                   kal.year = bound.getFullYear();              

                                   $extend(kal, this.values(kal));                     

                                   // TODO - IN THE CASE OF SELECT MOVE TO NEAREST VALID VALUE
                                   // IN THE CASE OF INPUT DISABLE

                                   // if new date is not valid better unset cal value
                                   // otherwise it would mean incrementally checking to find the nearest valid date which could be months / years away
                                   kal.val = kal.days.contains(bound.getDate()) ? bound : null;

                                   this.write(kal);

                                   if (kal.visible) { this.display(kal); } // update cal graphic if visible
                            }
                     }
                     else {
                            kal.month = cal.month;
                            kal.year = cal.year;
                     }
              }, this);
       },
       

       // clicked: run when a valid day is clicked in the calendar
       // @param cal (obj)

       clicked: function(td, day, cal) {
              cal.val = (this.value(cal) == day) ? null : new Date(cal.year, cal.month, day); // set new value - if same then disable

              this.write(cal); 

              // ok - in the special case that it's all selects and there's always a date no matter what (at least as far as the form is concerned)
              // we can't let the calendar undo a date selection - it's just not possible!!
              if (!cal.val) { cal.val = this.read(cal); }

              if (cal.val) {
                     this.check(cal); // checks other cals                                          
                     this.toggle(cal); // hide cal
              } 
              else { // remove active class and replace with valid
                     td.addClass(this.classes.valid);
                     td.removeClass(this.classes.active);
              }
       },
       

       // display: create calendar element
       // @param cal (obj)

       display: function(cal) {
              // 1. header and navigation
              this.calendar.empty(); // init div

              this.calendar.className = this.classes.calendar + ' ' + this.options.months[cal.month].toLowerCase();

              var div = new Element('div').injectInside(this.calendar); // a wrapper div to help correct browser css problems with the caption element

              var table = new Element('table').injectInside(div).adopt(this.caption(cal));
                            
              // 2. day names              
              var thead = new Element('thead').injectInside(table);

              var tr = new Element('tr').injectInside(thead);
              
              for (var i = 0; i <= 6; i++) {
                     var th = this.options.days[(i + this.options.offset) % 7];
                     
                     tr.adopt(new Element('th', { 'title': th }).appendText(th.substr(0, 1)));
              }

              // 3. day numbers
              var tbody = new Element('tbody').injectInside(table);
              var tr = new Element('tr').injectInside(tbody);

              var d = new Date(cal.year, cal.month, 1);
              var offset = ((d.getDay() - this.options.offset) + 7) % 7; // day of the week (offset)
              var last = new Date(cal.year, cal.month + 1, 0).getDate(); // last day of this month
              var prev = new Date(cal.year, cal.month, 0).getDate(); // last day of previous month
              var active = this.value(cal); // active date (if set and within curr month)
              var valid = cal.days; // valid days for curr month
              var inactive = []; // active dates set by other calendars
              var hilited = [];
              this.calendars.each(function(kal, i) {
                     if (kal != cal && kal.val) {
                            if (cal.year == kal.val.getFullYear() && cal.month == kal.val.getMonth()) { inactive.push(kal.val.getDate()); }

                            if (cal.val) {
                                   for (var day = 1; day <= last; day++) {
                                          d.setDate(day);
                                          
                                          if ((i < cal.id && d > kal.val && d < cal.val) || (i > cal.id && d > cal.val && d < kal.val)) { 
                                                 if (!hilited.contains(day)) { hilited.push(day); }
                                          }
                                   }
                            }
                     }
              }, this);
              var d = new Date();
              var today = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime(); // today obv 
              
              for (var i = 1; i < 43; i++) { // 1 to 42 (6 x 7 or 6 weeks)
                     if ((i - 1) % 7 == 0) { tr = new Element('tr').injectInside(tbody); } // each week is it's own table row

                     var td = new Element('td').injectInside(tr);
                                          
                     var day = i - offset;
                     var date = new Date(cal.year, cal.month, day);
                     
                     var cls = '';
                     
                     if (day === active) { cls = this.classes.active; } // active
                     else if (inactive.contains(day)) { cls = this.classes.inactive; } // inactive
                     else if (valid.contains(day)) { cls = this.classes.valid; } // valid
                     else if (day >= 1 && day <= last) { cls = this.classes.invalid; } // invalid

                     if (date.getTime() == today) { cls = cls + ' ' + this.classes.today; } // adds class for today

                     if (hilited.contains(day)) { cls = cls + ' ' + this.classes.hilite; } // adds class if hilited

                     td.addClass(cls);

                     if (valid.contains(day)) { // if it's a valid - clickable - day we add interaction
                            td.setProperty('title', this.format(date, 'D M jS Y'));
                            
                            td.addEvents({
                                   'click': function(td, day, cal) { 
                                          this.clicked(td, day, cal); 
                                   }.pass([td, day, cal], this),
                                   'mouseover': function(td, cls) { 
                                          td.addClass(cls); 
                                   }.pass([td, this.classes.hover]),
                                   'mouseout': function(td, cls) { 
                                          td.removeClass(cls); 
                                   }.pass([td, this.classes.hover])
                            });
                     }

                     // pad calendar with last days of prev month and first days of next month
                     if (day < 1) { day = prev + day; }
                     else if (day > last) { day = day - last; }

                     td.appendText(day);
              }
       },


       // element: helper function
       // @param el (string) element id
       // @param f (string) format string
       // @param cal (obj)

       element: function(el, f, cal) {
              if ($type(f) == 'object') { // in the case of multiple inputs per calendar
                     for (var i in f) { 
                            if (!this.element(i, f[i], cal)) { return false; }              
                     }
                     
                     return true;
              }

              el = $(el);

              if (!el) { return false; }
              
              el.format = f;
              
              if (el.getTag() == 'select') { // select elements allow the user to manually set the date via select option
                     el.addEvent('change', function(cal) { this.changed(cal); }.pass(cal, this));
              }
              else { // input (type text) elements restrict the user to only setting the date via the calendar
                     el.readOnly = true;
                     el.addEvent('focus', function(cal) { this.toggle(cal); }.pass(cal, this));
              }

              cal.els.push(el);

              return true;
       },


       // format: formats a date object according to passed in instructions
       // @param date (obj)
       // @param f (string) any combination of punctuation / separators and d, j, D, l, S, m, n, F, M, y, Y
       // @returns string

       format: function(date, format) {
              var str = '';
              
              if (date) {
                     var j = date.getDate(); // 1 - 31
      var w = date.getDay(); // 0 - 6
                     var l = this.options.days[w]; // Sunday - Saturday
                     var n = date.getMonth() + 1; // 1 - 12
                     var f = this.options.months[n - 1]; // January - December
                     var y = date.getFullYear() + ''; // 19xx - 20xx
                     
                     for (var i = 0, len = format.length; i < len; i++) {
                            var cha = format.charAt(i); // format char
                            
                            switch(cha) {
                                   // year cases
                                   case 'y': // xx - xx
                                          y = y.substr(2);
                                   case 'Y': // 19xx - 20xx
                                          str += y;
                                          break;
       
                                   // month cases
                                   case 'm': // 01 - 12
                                          if (n < 10) { n = '0' + n; }
                                   case 'n': // 1 - 12
                                          str += n;
                                          break;
       
                                   case 'M': // Jan - Dec
                                          f = f.substr(0, 3);
                                   case 'F': // January - December
                                          str += f;
                                          break;
       
                                   // day cases
                                   case 'd': // 01 - 31
                                          if (j < 10) { j = '0' + j; }
                                   case 'j': // 1 - 31
                                          str += j;
                                          break;
       
                                   case 'D': // Sun - Sat
                                          l = l.substr(0, 3);
                                   case 'l': // Sunday - Saturday
                                          str += l;
                                          break;
       
                                   case 'N': // 1 - 7
                                          w += 1;
                                   case 'w': // 0 - 6
                                          str += w;
                                          break;

                                   case 'S': // st, nd, rd or th (works well with j)
                                          if (j % 10 == 1 && j != '11') { str += 'st'; }
                                          else if (j % 10 == 2 && j != '12') { str += 'nd'; }
                                          else if (j % 10 == 3 && j != '13') { str += 'rd'; }
                                          else { str += 'th'; }
                                          break;
       
                                   default:
                                          str += cha;
                            }
                     }
              }

         return str; //  return format with values replaced
       },


       // navigate: calendar navigation
       // @param cal (obj)
       // @param type (str) m or y for month or year
       // @param n (int) + or - for next or prev

       navigate: function(cal, type, n) {
              switch (type) {
                     case 'm': // month
                                   if ($type(cal.months) == 'array') {
                                          var i = cal.months.indexOf(cal.month) + n; // index of current month
                                          
                                          if (i < 0 || i == cal.months.length) { // out of range
                                                 if (this.options.navigation == 1) { // if type 1 nav we'll need to increment the year
                                                        this.navigate(cal, 'y', n);              
                                                 }
              
                                                 i = (i < 0) ? cal.months.length - 1 : 0;
                                          }

                                          cal.month = cal.months[i];
                                   }
                                   else { 
                                          var i = cal.month + n;
              
                                          if (i < 0 || i == 12) {
                                                 if (this.options.navigation == 1) {
                                                        this.navigate(cal, 'y', n);       
                                                 }
              
                                                 i = (i < 0) ? 11 : 0;
                                          }
                                          
                                          cal.month = i;
                                   }              
                                   break;

                            case 'y': // year
                                   if ($type(cal.years) == 'array') {
                                          var i = cal.years.indexOf(cal.year) + n;

                                          cal.year = cal.years[i]; 
                                   }
                                   else { 
                                          cal.year += n;
                                   }                                          
                                   break;              
              }

              $extend(cal, this.values(cal));

              if ($type(cal.months) == 'array') { // if the calendar has a months select
                     var i = cal.months.indexOf(cal.month); // and make sure the curr months exists for the new year

                     if (i < 0) { cal.month = cal.months[0]; } // otherwise we'll reset the month
              }


              this.display(cal);
       },


       // read: compiles cal value based on array of inputs passed in
       // @param cal (obj)
       // @returns date (obj) or (null)

       read: function(cal) {
              var arr = [null, null, null];

              cal.els.each(function(el) {
                     // returns an array which may contain empty values
                     var values = this.unformat(el.value, el.format);
                     
                     values.each(function(val, i) { 
                            if ($type(val) == 'number') { arr[i] = val; }
                     }); 
              }, this);

              // we can update the cals month and year values
              if ($type(arr[0]) == 'number') { cal.year = arr[0]; }
              if ($type(arr[1]) == 'number') { cal.month = arr[1]; }

              var val = null;

              if (arr.every(function(i) { return $type(i) == 'number'; })) { // if valid date
                     var last = new Date(arr[0], arr[1] + 1, 0).getDate(); // last day of month

                     if (arr[2] > last) { arr[2] = last; } // make sure we stay within the month (ex in case default day of select is 31 and month is feb)
                     
                     val = new Date(arr[0], arr[1], arr[2]);
              }

              return (cal.val == val) ? null : val; // if new date matches old return null (same date clicked twice = disable)
       },

       
       // rebuild: rebuilds days + months selects
       // @param cal (obj)

       rebuild: function(cal) {
              cal.els.each(function(el) {                     
                     /*
                     if (el.getTag() == 'select' && el.format.test('^(F|m|M|n)$')) { // special case for months-only select
                            if (!cal.options) { cal.options = el.clone(); } // clone a copy of months select
                     
                            var val = (cal.val) ? cal.val.getMonth() : el.value.toInt();

                            el.empty(); // initialize select

                            cal.months.each(function(month) {
                                   // create an option element
                                   var option = new Element('option', {
                                          'selected': (val == month),
                                          'value': this.format(new Date(1, month, 1), el.format);
                                   }).appendText(day).injectInside(el);
                            }, this);
                     }
                     */

                     if (el.getTag() == 'select' && el.format.test('^(d|j)$')) { // special case for days-only select
                            var d = this.value(cal);

                            if (!d) { d = el.value.toInt(); } // if the calendar doesn't have a set value, try to use value from select

                            el.empty(); // initialize select

                            cal.days.each(function(day) {
                                   // create an option element
                                   var option = new Element('option', {
                                          'selected': (d == day),
                                          'value': ((el.format == 'd' && day < 10) ? '0' + day : day)
                                   }).appendText(day).injectInside(el);
                            }, this);
                     }
              }, this); 
       },


       // sort: helper function for numerical sorting

       sort: function(a, b) {
              return a - b;
       },


       // toggle: show / hide calendar 
       // @param cal (obj)

       toggle: function(cal) {
              document.removeEvent('mousedown', this.fn); // always remove the current mousedown script first
                     
              if (cal.visible) { // simply hide curr cal                                          
                     cal.visible = false;
                     cal.button.removeClass(this.classes.active); // active
                     
                     this.fx.start(1, 0);
              }
              else { // otherwise show (may have to hide others)
                     // hide cal on out-of-bounds click
                     this.fn = function(e, cal) { 
                            var e = new Event(e);
                     
                            var el = e.target;

                            var stop = false;
                            
                            while (el != document.body && el.nodeType == 1) {
                                   if (el == this.calendar) { stop = true; }
                                   this.calendars.each(function(kal) {
                                          if (kal.button == el || kal.els.contains(el)) { stop = true; }
                                   });

                                   if (stop) { 
                                          e.stop();
                                          return false;
                                   }
                                   else { el = el.parentNode; }
                            }
                            
                            this.toggle(cal);
                     }.create({ 'arguments': cal, 'bind': this, 'event': true });                            

                     document.addEvent('mousedown', this.fn);

                     this.calendars.each(function(kal) {
                            if (kal == cal) {
                                   kal.visible = true;
                                   kal.button.addClass(this.classes.active); // css c-icon-active
                            }
                            else {
                                   kal.visible = false;
                                   kal.button.removeClass(this.classes.active); // css c-icon-active
                            }
                     }, this);
                     
                     var size = window.getSize().scrollSize;
                     
                     var coord = cal.button.getCoordinates();

                     var x = coord.right + this.options.tweak.x;
                     var y = coord.top + this.options.tweak.y;

                     // make sure the calendar doesn't open off screen
                     if (!this.calendar.coord) { this.calendar.coord = this.calendar.getCoordinates(); }

                     if (x + this.calendar.coord.width > size.x) { x -= (x + this.calendar.coord.width - size.x); }
                     if (y + this.calendar.coord.height > size.y) { y -= (y + this.calendar.coord.height - size.y); }
                     
                     this.calendar.setStyles({ left: x + 'px', top: y + 'px' });

                     if (window.ie6) { 
                            this.iframe.setStyles({ height: this.calendar.coord.height + 'px', left: x + 'px', top: y + 'px', width: this.calendar.coord.width + 'px' }); 
                     }

                     this.display(cal);
                     
                     this.fx.start(0, 1);
              }
       },


       // unformat: takes a value from an input and parses the d, m and y elements
       // @param val (string)
       // @param f (string) any combination of punctuation / separators and d, j, D, l, S, m, n, F, M, y, Y
       // @returns array
       
       unformat: function(val, f) {
              f = f.escapeRegExp();
              
              var re = {
                     d: '([0-9]{2})',
                     j: '([0-9]{1,2})',
                     D: '(' + this.options.days.map(function(day) { return day.substr(0, 3); }).join('|') + ')',                                   
                     l: '(' + this.options.days.join('|') + ')',
                     S: '(st|nd|rd|th)',
                     F: '(' + this.options.months.join('|') + ')',
                     m: '([0-9]{2})',
                     M: '(' + this.options.months.map(function(month) { return month.substr(0, 3); }).join('|') + ')',                                   
                     n: '([0-9]{1,2})',
                     Y: '([0-9]{4})',
                     y: '([0-9]{2})'
              }

              var arr = []; // array of indexes

              var g = '';

              // convert our format string to regexp
              for (var i = 0; i < f.length; i++) {
                     var c = f.charAt(i);
                     
                     if (re[c]) {
                            arr.push(c);

                            g += re[c];
                     }
                     else {
                            g += c;
                     }
              }

              // match against date
              var matches = val.match('^' + g + '$');
              
              var dates = new Array(3);

              if (matches) {
                     matches = matches.slice(1); // remove first match which is the date

                     arr.each(function(c, i) {
                            i = matches[i];
                            
                            switch(c) {
                                   // year cases
                                   case 'y':
                                          i = '19' + i; // 2 digit year assumes 19th century (same as JS)
                                   case 'Y':
                                          dates[0] = i.toInt();
                                          break;

                                   // month cases
                                   case 'F':
                                          i = i.substr(0, 3);
                                   case 'M':
                                          i = this.options.months.map(function(month) { return month.substr(0, 3); }).indexOf(i) + 1;
                                   case 'm':
                                   case 'n':
                                          dates[1] = i.toInt() - 1;
                                          break;

                                   // day cases
                                   case 'd':
                                   case 'j':
                                          dates[2] = i.toInt();
                                          break;
                            }
                     }, this);
              }

              return dates;
       },


       // value: returns day value of calendar if set
       // @param cal (obj)
       // @returns day (int) or null

       value: function(cal) {
              var day = null;

              if (cal.val) {
                     if (cal.year == cal.val.getFullYear() && cal.month == cal.val.getMonth()) { day = cal.val.getDate(); }
              }

              return day;
       },
       

       // values: returns the years, months (for curr year) and days (for curr month and year) for the calendar
       // @param cal (obj)
       // @returns obj       

       values: function(cal) {
              var years, months, days;

              cal.els.each(function(el) {       
                     if (el.getTag() == 'select') {              
                            if (el.format.test('(y|Y)')) { // search for a year select
                                   years = [];

                                   el.getChildren().each(function(option) { // get options
                                          var values = this.unformat(option.value, el.format);
       
                                          if (!years.contains(values[0])) { years.push(values[0]); } // add to years array
                                   }, this);
       
                                   years.sort(this.sort);
                            }
       
                            if (el.format.test('(F|m|M|n)')) { // search for a month select
                                   months = []; // 0 - 11 should be

                                   el.getChildren().each(function(option) { // get options
                                          var values = this.unformat(option.value, el.format);
       
                                          if ($type(values[0]) != 'number' || values[0] == cal.year) { // if it's a year / month combo for curr year, or simply a month select
                                                 if (!months.contains(values[1])) { months.push(values[1]); } // add to months array
                                          }
                                   }, this);
       
                                   months.sort(this.sort);
                            }
                            
                            if (el.format.test('(d|j)') && !el.format.test('^(d|j)$')) { // search for a day select, but NOT a days only select
                                   days = []; // 1 - 31
                                   
                                   el.getChildren().each(function(option) { // get options
                                          var values = this.unformat(option.value, el.format);

                                          // in the special case of days we dont want the value if its a days only select
                                          // otherwise that will screw up the options rebuilding
                                          // we will take the values if they are exact dates though
                                          if (values[0] == cal.year && values[1] == cal.month) {
                                                 if (!days.contains(values[2])) { days.push(values[2]); } // add to days array
                                          }
                                   }, this);
                            }
                     }
              }, this);
              
              // we start with what would be the first and last days were there no restrictions
              var first = 1;
              var last = new Date(cal.year, cal.month + 1, 0).getDate(); // last day of the month
              
              // if we're in an out of bounds year
              if (cal.year == cal.start.getFullYear()) {
                     // in the special case of improved navigation but no months array, we'll need to construct one
                     if (months == null && this.options.navigation == 2) {
                            months = [];
                            
                            for (var i = 0; i < 12; i ++) { 
                                   if (i >= cal.start.getMonth()) { months.push(i); } 
                            }
                     }
                     
                     // if we're in an out of bounds month
                     if (cal.month == cal.start.getMonth()) { 
                            first = cal.start.getDate(); // first day equals day of bound
                     }
              }              
              if (cal.year == cal.end.getFullYear()) {
                     // in the special case of improved navigation but no months array, we'll need to construct one
                     if (months == null && this.options.navigation == 2) {
                            months = [];
                            
                            for (var i = 0; i < 12; i ++) { 
                                   if (i <= cal.end.getMonth()) { months.push(i); } 
                            }
                     }

                     if (cal.month == cal.end.getMonth()) { 
                            last = cal.end.getDate(); // last day equals day of bound
                     }
              }

              // let's get our invalid days
              var blocked = this.blocked(cal);

              // finally we can prepare all the valid days in a neat little array
              if ($type(days) == 'array') { // somewhere there was a days select
                     days = days.filter(function(day) {
                            if (day >= first && day <= last && !blocked.contains(day)) { return day; }
                     });
              }
              else { // no days select we'll need to construct a valid days array
                     days = [];
                     
                     for (var i = first; i <= last; i++) { 
                            if (!blocked.contains(i)) { days.push(i); }
                     }
              }              

              days.sort(this.sort); // sorting our days will give us first and last of month

              return { 'days': days, 'months': months, 'years': years };
       },


       // write: sets calendars value to form elements
       // @param cal (obj)

       write: function(cal) {
              this.rebuild(cal);        // in the case of options, we'll need to make sure we have the correct number of days available
              
              cal.els.each(function(el) {       // then we can set the value to the field
                     el.value = this.format(cal.val, el.format);               
              }, this);
       }
});

Calendar.implement(new Events, new Options);
