// Metodus: Internet Productivo, Usabilidad y Desarrollo Web http://www.metodus.com
// License: http://www.opensource.org/licenses/mit-license.php
// Calendar: v2.0 by Aeron Glemann http://www.electricprism.com/aeron

Calendar = Class.create();
Calendar.prototype = {
  initialize: function(input, props) {
    this.props = {
      debug: false,
      offset: 1,
      lang: 'en',
      past: false,
      future: true,
      format: 'd',
      drag: true,
			pad: 2,
      classes: new Array('c-icon', 'c-icon-active', 'c-div', 'c-th-prev', 'c-th-next', 'c-td-heading', 'c-td-invalid', 'c-td-valid', 'c-td-hover', 'c-td-active')
    }
    Object.extend(this.props, props || {});  

    if (this.props.lang == 'en') {
      this.weekdays = new Array('S', 'S', 'M', 'T', 'W', 'T', 'F');
      this.months = new Array('January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December');
    }
    else {
      this.weekdays = new Array('S', 'D', 'L', 'M', 'M', 'J', 'V');
      this.months = new Array('Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre');
    }

    this.mDays = new Array(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
    this.calendars = new Array();

    d = new Date();
    this.year = d.getYear();
    if (this.year < 1900) this.year += 1900;
    this.month = d.getMonth() + 1;
    this.today = d.getDate();

    this.inputs = input.replace(/ /g, '').split(',');

    for (i = 0; i < this.inputs.length; i++) {
      // create calendar button
      a = document.createElement('a');
      a.className = this.props.classes[0]; // css c-icon
      a.onclick = (function(z) { return function() { this.toggle(z); }})(i).bind(this);

      input = $(this.inputs[i]);
      // input may be select
      if (input.tagName == 'SELECT') {
        pn = input.parentNode;
        // in the case of selects there may be more than one
        selects = pn.getElementsByTagName('select');
        for (j = 0; j < selects.length; j++) {
          select = selects[j];
          if (!select.format) select.format = this.props.format;
          select.onchange = (function(z) { return function() { this.change(z); }})(i).bind(this);          
        }
      } else {
        selects = false;
        // if input is text than disable field
        input.readOnly = true;
        input.onfocus = (function(z) { return function() { this.toggle(z); }})(i).bind(this);
      }
      // append calendar button
      input.parentNode.insertBefore(a, input.nextSibling);
			input.className = input.className + ' c';

      // create calendar array
      calendar = new Array();
      calendar['debug'] = i;
      calendar['a'] = a; // button reference
      calendar['input'] = input; // input reference
      calendar['selects'] = selects; // if selects
      if (selects) calendar['value'] = this.getValue(selects);
      else calendar['value'] = this.unformat(input.value);
      // set starting values based on calendar value
      if (calendar['value'].length) { 
        v = calendar['value'].split(',');
        calendar['year'] = parseInt(v[2], 10);
        calendar['month'] = parseInt(v[1], 10);
      }
      // if no value set starting values based on today
      else {
        calendar['year'] = this.year;
        calendar['month'] = this.month;
      }
      calendar['display'] = 'none'; // hidden by default

      this.calendars.push(calendar); // add to calendars array
      if (selects) this.click(calendar, calendar['value']); // update select options
    }

    // create calendar element
    // there may be multiple inputs but only one calendar element per instance
    this.div = document.createElement('div');
    this.div.className = this.props.classes[2]; // css c-div
    this.div.style.position = 'absolute';
    this.div.style.zIndex = 1000;
    if (window.fx) {  
      this.fx = new fx.Opacity(this.div, { duration: 500 });
      this.fx.hide();
    }
    else this.div.style.display = 'none';
    
    // initialize drag method
    if (this.props.drag) {
      this.div.onmousedown =   this.dragStart.bind(this);
      this.div.onDragStart = new Function();
      this.div.onDragEnd = new Function();
      this.div.onDrag = new Function();
    }

    // add calendar element to document
    document.body.appendChild(this.div);
  },

  // callable function to initialize calendar based on default values in inputs
  reset: function() {
    for (i = 0; i < this.inputs.length; i++) {      
      calendar = this.calendars[i];
      
      input = calendar['input'];
      selects = calendar['selects'];
      
      if (selects) calendar['value'] = this.getValue(selects);
      else calendar['value'] = this.unformat(input.value);
      if (calendar['value'].length) {
        v = calendar['value'].split(',');
        calendar['year'] = parseInt(v[2], 10);
        calendar['month'] = parseInt(v[1], 10);
      }
      else {
        calendar['year'] = this.year;
        calendar['month'] = this.month;
      }
    }
  },

  toggle: function(n) {
    calendar = this.calendars[n];

    if (calendar['display'] == 'none') { // show calendar
      // hide calendar on out-of-bounds click
      document.onmousedown = (function(z) { return function(e) { 
        if (!e) e = window.event;
        if (e.target) el = e.target;
        else if(e.srcElement) el = e.srcElement;
        if (el.nodeType == 3) el = el.parentNode; // defeat safari bug ??
        
        while (el != document.body) {
          if (el == this.div) return;
          for (i = 0; i < this.calendars.length; i++) {
            if (el == this.calendars[i]['a'] || el == this.calendars[i]['input']) return;
          }
          el = el.parentNode;
        }
        
        this.toggle(z);
      }})(n).bind(this);

      for (i = 0; i < this.calendars.length; i++) {
        if (this.calendars[i] == calendar) {
          calendar['display'] = 'block';
          calendar['a'].className = this.props.classes[0] + ' ' + this.props.classes[1]; // css c-icon and c-icon-active
        }
        else {
          this.calendars[i]['display'] = 'none';
          this.calendars[i]['a'].className = this.props.classes[0]; // css c-icon
        }
      }
      
      p = Position.cumulativeOffset(calendar['a']);
      p[0] = p[0] + this.getStyle(calendar['a'], 'width');
  
      this.div.style.left = p[0] + 'px';
      this.div.style.top = p[1] + 'px';

      for (i = 0; i < this.calendars.length; i++) {
        if (i != n && this.calendars[i]['value'].length && !calendar['value'].length) {
          d = this.calendars[i]['value'].split(',');
            
          calendar['month'] = parseInt(d[1], 10);
          calendar['year'] = parseInt(d[2], 10);
        }
      }

      this.create(calendar);
    }
    else { // hide calendar
      document.onmousedown = null;

      calendar['display'] = 'none';
      calendar['a'].className = this.props.classes[0]; // css c-icon
    }

    if (window.fx) {
      if (calendar['display'] == 'none' && this.fx.now > 0) this.fx.custom(1, 0);
      else if (this.fx.now == 0) this.fx.custom(0, 1);
    }
    else this.div.style.display = calendar['display'];    
  },

  change: function(cal, v) {
    if (typeof(cal) == 'number') cal = this.calendars[cal];
    
    if (!v) {
      if (cal['selects']) v = this.getValue(cal['selects']);
      else v = this.unformat(cal['input'].value);
    }

    this.click(cal, v);

    v = cal['value'].split(',');
    var d = parseInt(v[0], 10);
    var m = parseInt(v[1], 10);
    var y = parseInt(v[2], 10); 
    
    // 2. ensure that other calendars contain valid values
    for (var c = 2; c < this.calendars.length; c++) {
      if (c == cal['debug']) continue;

 	   	var date = new Date(y, (m - 1), d);

     	v = this.calendars[c]['value'].split(',');
      v[0] = parseInt(v[0], 10);
      v[1] = parseInt(v[1], 10);
      v[2] = parseInt(v[2], 10);
			var debug = new Date(v[2], (v[1] - 1), v[0]);
        
      if (c < cal['debug']) { // less
				date.setDate(d - this.props.pad);
				if (debug > date){				
					v[0] = date.getDate();
					v[1] = date.getMonth() + 1;
					v[2] = date.getFullYear();
	        this.click(this.calendars[c], v.join(','));
				}
      }
      
      if (c > cal['debug']) { // greater        
				date.setDate(d + this.props.pad);
				if (debug < date){
					v[0] = date.getDate();
					v[1] = date.getMonth() + 1;
					v[2] = date.getFullYear();
	        this.click(this.calendars[c], v.join(','));
				}
      }
    }    
  },

  click: function(cal, v) {
    v = v.split(',');

    if (cal['selects']) {
      v[0] = parseInt(v[0], 10); // day
      v[1] = parseInt(v[1], 10); // month
      v[2] = parseInt(v[2], 10); // year

      // set first selectable day of the month
      var start = 1;
      if (v[1] == this.month && !this.props.past) start = this.today;
  
      // set last selectable day of the month
      var stop = (this.isLeapYear(v[2]) && v[1] == 2) ? 29 : this.mDays[v[1] - 1]; // account for leap year
      if (v[1] == this.month && !this.props.future) stop = this.today;
  
      // adjust day value if not in range of allowed days
      if (v[0] < start) v[0] = start;
      if (v[0] > stop) v[0] = stop;
  
      for (s = 0; s < cal['selects'].length; s++) {
        var select = cal['selects'][s];
        select.value = this.format(v[0], v[1], v[2], select.format);
        // if day select
        if (select.format == 'd') {
          select.innerHTML = ''; // initialize select
          
          // for each day
          for (day = start; day <= stop; day++) {
            // create an option element
            option = document.createElement('option');
            option.value = (day < 10) ? '0' + day : String(day);
            option.appendChild(document.createTextNode(String(day)));            
            select.appendChild(option);
            if (day == (v[0])) option.selected = 'selected';
          }
        }
      }
    }
    else cal['input'].value = this.format(v[0], v[1], v[2]);

    cal['value'] = v[0] + ',' + v[1] + ',' + v[2];
    cal['year'] = v[2];
    cal['month'] = v[1];
  },

  // navigation functions  
  prev: function(cal) {
    cal['month']--;
    if (cal['month'] < 1) {
      cal['month'] = 12;
      cal['year']--;
    }
    
    this.create(cal);    
  },

  next: function(cal) {
    cal['month']++;
    if (cal['month'] > 12) {
      cal['month'] = 1;
      cal['year']++;      
    }

    this.create(cal);
  },

  // formatting functions  
  unformat: function(v, f) {
    if (!v || !v.length) return '';
    
    f = (f) ? f : this.props.format;
    u = '';

    x = new Array();
    y = new Array();    
    
    while (f.length) {
      c = f.substr(0, 1);
      f = f.substr(1);

      if (/d|m|y/.test(c)) {
        n = 0;
        while (f.substr(0, 1) == c) {
          n++;
          f = f.substr(1);
        }
        if (n > 0) {
          d = v.substr(u.length, (n + 1));
        }
        else if (f.length == 0) {
          d = v.substring(u.length, v.length);
        }
        else if (!/d|m|y/.test(f.substr(0, 1))) {
          d = v.substring(u.length, v.indexOf(f.substr(0, 1), u.length));
        }
        else {
          if (this.props.lang == "en") return alert("Parameter 'format' must contain exact units (mm, dd, etc) or separator characters (/, -, etc)");
          else return alert("El parametro 'format' debe contener unidades exactas (mm, dd, etc) o caracteres tipo separador (/, -, etc)");
        }

        u = u + d;

        x.push(d);
        y.push(c);
      }
      else u = u + c;
    }

    f = "d,m,y";

    for (k = 0; k < x.length; k++) {
      f = f.replace(y[k], x[k]);    
    }

    f = f.replace(/d|m|y/g,'');

    return f
  },

  // format can be:
  // d (1 - 31), dd (01 - 31), ddd (001 - 031) etc..
  // m (1 - 12), mm (01 - 12), mmm (001 - 012) etc..
  // y or yyyy (1999, 2000, etc..), yy (99, 00, etc..), yyy (999, 000, etc..) etc..
  // differentiated by seperator characters (/- etc..) such as 'dd/mm/yyyy' or 'ddmmyyyy' or 'd-m-y' or 'mm,y' or whatever
  format: function(d, m, y, f) {
    // if no format param is passed use the default value
    f = (f) ? f : this.props.format;

    // values for day, month, year
    var x = new Array(d, m, y);
    // symbols for day, month, year
    var y = new Array('d', 'm', 'y');

    // iterate for each date measure (d, m, y)
    for (k = 0; k < 3; k++) {
      var num = 0; // number of times the symbol appears
      var p = f.indexOf(y[k]);
      while (p != -1) {
        num++;
        p = f.indexOf(y[k], p + 1);
      }
      if (num > 1) { // exact specification - ie dd, mm, yyyy etc..
        x[k] = String(x[k]); 
        while (x[k].length < num) x[k] = '0' + x[k]; // pad the value if needed
        var re = new RegExp(y[k] + '+');
        f = f.replace(re, x[k].substring(x[k].length - num, x[k].length)); // replace instance of symbol in format
      }
      else f = f.replace(y[k], x[k]); // simple specifications - ie d, m, y
    }

    return f; //  return format with values replaced
  },

  // helper functions
  isLeapYear: function(year) {
    return (!(year % 4) && (year < 1582 || year % 100 || !(year % 400))) ? true : false;
  },

  getStyle: function(el, prop) {
    if (el.style[prop]) {
      v = el.style[prop];
    } 
    else if (el.currentStyle) {
      v = el.currentStyle[prop];
    } 
    else if (document.defaultView && document.defaultView.getComputedStyle) {
      re = /([A-Z])/g;
      prop = prop.replace(re, "-$1");
      prop = prop.toLowerCase();
      v = document.defaultView.getComputedStyle(el,"").getPropertyValue(prop);
    } 
    else return null;
    
    return parseInt(v, 10);
  },

  getEvent: function(e) {
    if (typeof e == 'undefined') e = window.event;
    if (typeof e.layerX == 'undefined') e.layerX = e.offsetX;
    if (typeof e.layerY == 'undefined') e.layerY = e.offsetY;
    
    return e;
  },

  getWeekday: function(year, days) {
    d = days;
    if (year) d += (year - 1) * 365;
    for (i = 1; i < year; i++) if (this.isLeapYear(i)) d++;
    if (year > 1582 || (year == 1582 && days >= 277)) d -= 10;
    if (d) d = (d - this.props.offset) % 7;
    else if (this.props.offset) d += 7 - this.props.offset;

    return d;
  },

  getValue: function(selects) {
    // day, month, year
    b = new Array(3);

    // iterate for each select
    for (s = 0; s < selects.length; s++) {
      select = selects[s];

      // get select format (d, m, y)
      f = select.format;  
      // returns an array [d, m, y] may contain empty values
      a = this.unformat(select.value, f).split(',');

      // combine arrays to overwrite empty values
      for (j = 0; j < 3; j++) if (a[j].length) b[j] = a[j];
    }

    // return array as string
    return b.join(',');
  },

  // drag functions  
  dragStart: function(e) {
    e = this.getEvent(e);
    y = parseInt(this.div.style.top, 10);
    x = parseInt(this.div.style.left, 10);
    
    this.div.onDragStart(x, y);
    this.div.lastMouseX = e.clientX;
    this.div.lastMouseY = e.clientY;
    
    document.onmousemove = this.drag.bind(this);
    document.onmouseup = this.dragEnd.bind(this);
    
    return false;
  },
  
  drag: function(e) {
    e = this.getEvent(e);
    
    ey = e.clientY;
    ex = e.clientX;
    x = parseInt(this.div.style.left, 10);
    y = parseInt(this.div.style.top, 10);
    
    nx = x + ((ex - this.div.lastMouseX));
    ny = y + ((ey - this.div.lastMouseY));
    
    this.div.style.left = nx + "px";
    this.div.style.top = ny + "px";
    this.div.lastMouseX = ex;
    this.div.lastMouseY = ey;    
    this.div.onDrag(nx, ny);
    
    return false;
  },
  
  dragEnd: function() {
    document.onmousemove = null;
    document.onmouseup = null;
    
    this.div.onDragEnd(parseInt(this.div.style.left, 10), parseInt(this.div.style.top, 10));
  },
  
  // calendar create functions  
  create: function(cal) {
    n = cal['debug']; // index

    inLessMonth = inGreaterMonth = invalidMonth = activeDay = hoverDay = false;

    for (i = 0; i < this.calendars.length; i++) {
      v = this.calendars[i]['value'].split(',');
      v[0] = parseInt(v[0], 10);
      v[1] = parseInt(v[1], 10);
      v[2] = parseInt(v[2], 10);
      
      if (i != n && this.calendars[i]['value'].length) {
        activeDay = v[0];
          
        if (i < n) {
          if (cal['year'] < v[2] || (cal['year'] == v[2] && cal['month'] < v[1])) invalidMonth = true;
          if (cal['year'] == v[2] && cal['month'] == v[1]) inLessMonth = true;
        }
        if (i > n) {
          if (cal['year'] > v[2] || (cal['year'] == v[2] && cal['month'] > v[1])) invalidMonth = true;
          if (cal['year'] == v[2] && cal['month'] == v[1]) inGreaterMonth = true;
        }
      }
      else if (cal['year'] == v[2] && cal['month'] == v[1]) hoverDay = v[0];
    }
    
    (this.isLeapYear(cal['year'])) ? this.mDays[1] = 29 : this.mDays[1] = 28;
    for (i = days = 0; i < cal['month'] - 1; i++) days += this.mDays[i];

    if ((cal['year'] == this.year) && (cal['month'] == this.month)) inThisMonth = true;
    else inThisMonth = false;

    start = days;
    if (cal['year']) start += (cal['year'] - 1) * 365;
    for (i = 1; i < cal['year']; i++) if (this.isLeapYear(i)) start++;
    if (cal['year'] > 1582 || (cal['year'] == 1582 && days >= 277)) start -= 10;
    if (start) start = (start - this.props.offset) % 7;
    else if (this.props.offset) start += 7 - this.props.offset;

    stop = this.mDays[cal['month'] - 1];

    table = document.createElement('table');
    tbody = document.createElement('tbody');
    
    tbody.appendChild(this.th(cal, inThisMonth, inLessMonth, inGreaterMonth));

    tr = document.createElement('tr');
    
    for (i = 0; i <= 6; i++) {
      d = (i + this.props.offset) % 7;
      txt = this.weekdays[d];
      tr.appendChild(this.td(txt, this.props.classes[5], cal)); // css c-td-heading
    }
    tbody.appendChild(tr);

    daycount = 1;
    rowcount = 0;

    while(daycount <= stop || rowcount < 6) {
      tr = document.createElement('tr');
      rowcount++;      
      
      for (i = wdays = 0; i <= 6; i++) {
        // if ((inThisMonth && daycount < this.today && !this.props.past) || (inThisMonth && daycount > this.today && !this.props.future) || (inLessMonth && daycount < activeDay) || (inGreaterMonth && daycount > activeDay) || invalidMonth) cls = this.props.classes[6]; // css c-td-invalid
        if ((inThisMonth && daycount < this.today && !this.props.past) || (inThisMonth && daycount > this.today && !this.props.future)) cls = this.props.classes[6]; // css c-td-invalid
        else if ((inLessMonth && daycount == activeDay) || (inGreaterMonth && daycount == activeDay)) cls = this.props.classes[9]; // css c-td-active
        else if (daycount == hoverDay) cls = this.props.classes[8]; // css c-td-hover
        else cls = this.props.classes[7]; // css c-td-valid
        
        if ((daycount == 1 && i < start) || daycount > stop) txt = null;
        else {
          txt = daycount;
          daycount++;
          wdays++;
        }
        tr.appendChild(this.td(txt, cls, cal));
      }
      tbody.appendChild(tr);  
    }
    table.appendChild(tbody);

    if (this.div.childNodes.length) this.div.removeChild(this.div.firstChild);
    this.div.appendChild(table);

    if (this.props.debug) alert(this.div.innerHTML);
  },
  
  th: function(cal, inThisMonth, inLessMonth, inGreaterMonth) {
    tr = document.createElement('tr');
    
    th = document.createElement('th');
    th.className = this.props.classes[3]; // css c-th-prev
    // if ((!inThisMonth || this.props.past) && !inLessMonth) {
    if (!inThisMonth || this.props.past) {
      a = document.createElement('a');
      a.appendChild(document.createTextNode(unescape('%3C')));
      a.onclick = (function(z) { return function() { this.prev(z); }})(cal).bind(this);
      th.appendChild(a);
    }  
    tr.appendChild(th);
        
    th = document.createElement('th');
    th.colSpan = '5';
    th.appendChild(document.createTextNode(this.months[cal['month'] - 1] + ' ' + cal['year']));
    tr.appendChild(th);

    th = document.createElement('th');
    th.className = this.props.classes[4]; // css c-th-next
    // if ((!inThisMonth || this.props.future) && !inGreaterMonth) {
    if (!inThisMonth || this.props.future) {
      a = document.createElement('a');
      a.appendChild(document.createTextNode(unescape('%3E')));
      a.onclick = (function(z) { return function() { this.next(z); }})(cal).bind(this);
      th.appendChild(a);
    }
    tr.appendChild(th);
    
    return tr;
  },

  td: function(txt, cls, cal) {
    td = document.createElement('td');
    if (txt) td.className = cls;

    if (txt && cls == this.props.classes[7]) { // css c-td-valid
      n = cal['debug'];
      
      td.onmouseover = (function(z) { return function() { this.className = z; }})(this.props.classes[8]); // css c-td-hover
      td.onmouseout = function() { this.className = cls; };
      td.onclick = (function(z) { return function() {
        this.change(cal, txt + ',' + cal['month'] + ',' + cal['year']);
        this.toggle(z);
      }})(n).bind(this); 
    }
    text = (txt) ? document.createTextNode(txt) : document.createTextNode(unescape('%20')); // nbsp
    td.appendChild(text);

    return td;
  }
}