if (typeof Prototype == 'undefined')
  throw new Error('This package extends Prototype! See http://prototypejs.org/');

Object.extend(Prototype, {

  StyleFragment: '<style[^>]*>([\\S\\s]*?)<\/style>',

  LinkFragment: '<link[^>]*>'

});


Object.extend(Date, {

  // [seconds, minutes, hours, days, weeks]
  durationMultipliers: [1, 60, 3600, 86400, 604800],

  fromJSON: function(dt) {
    var m = dt.match(/^(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)(?:(Z)|([+-])(\d\d)(?::(\d\d))?)$/);
    if (!m) return null;
    var offset = 0;
    if (m[7] != 'Z') {
      offset += parseInt(m[9],10) * 60;
      offset += parseInt(m[10],10) || 0;
      if (m[8] == '+') offset *= -1;
    }
    m = m.splice(1,6).map(function(s){return parseInt(s,10)});
    m[1] -= 1;
    var milli = Date.UTC.apply(null,m) + offset * 60 * 1000;
    return new Date(milli);
  },

  today: function() {
    var t = new Date();
    t.setHours(0,0,0,0);
    return t;
  },
  tomorrow: function() {
    var t = new Date();
    t.setHours(24,0,0,0);
    return t;
  },
  yesterday: function() {
    var t = new Date();
    t.setHours(-24,0,0,0);
    return t;
  }

});


Object.extend(Date.prototype, {

  clone: function() {
    return new Date(this.getTime());
  },

  date: function() {
    return this.getFullYear()
      + '-' + (this.getMonth() + 1).toPaddedString(2)
      + '-' + this.getDate().toPaddedString(2);
  },

  time: function() {
    return this.getHours().toPaddedString(2)
      + ':' + this.getMinutes().toPaddedString(2)
      + ':' + this.getSeconds().toPaddedString(2);
  },

  datetime: function() {
    return this.date() + ' ' + this.time();
  },

  UTCTime: function() {
    return this.getUTCHours().toPaddedString(2)
      + ':' + this.getUTCMinutes().toPaddedString(2)
      + ':' + this.getUTCSeconds().toPaddedString(2);
  },

  UTCDate: function() {
    return this.getUTCFullYear()
      + '-' + (this.getUTCMonth() + 1).toPaddedString(2)
      + '-' + this.getUTCDate().toPaddedString(2);
  },

  timezone: function() {
    var offset = this.getTimezoneOffset();
    if (!offset) return 'Z';
    return (offset < 0 ? '+' : '-')
      + (offset.abs()/60).floor().toPaddedString(2)
      + ':' + (offset % 60).toPaddedString(2);
  },

  iso8601: function() {
    return this.date() + 'T' + this.time() + this.timezone();
  },

  thisDay: function() {
    var t = this.clone();
    t.setHours(0,0,0,0);
    return t;
  },
  nextDay: function() {
    var t = this.clone();
    t.setHours(24,0,0,0);
    return t;
  },
  previousDay: function() {
    var t = this.clone();
    t.setHours(-24,0,0,0);
    return t;
  },

  daysSinceEpoch: function() {
    return parseInt( this / 86400000, 10);
  },

  fuzzy: function() {
    var daysDiff = this.today().daysSinceEpoch() - Date.today().daysSinceEpoch();
    return ( daysDiff > 1 ? daysDiff : '' )
      + ( daysDiff > 0 ? '+' : '' )
      + this.getHours().toPaddedString(2)
      + ':' + this.getMinutes().toPaddedString(2);
  }

});


Object.extend(Number.prototype, {

  toDuration: function(microPrecision, macroPrecision) {
    if (this < 60) return this.toString();
    var d = [],
        n = (this + 0), // lame clone :)
        m = Date.durationMultipliers,
        i = m.length - (macroPrecision || 0);
    while(i--) {
      x = Math.floor(n/m[i]), n %= m[i];
      if (x || d.length) d.push( x.toPaddedString(2) );
    }
    d = d.join(':')
    if (!microPrecision || !n) return d;
    return d + (n - Math.floor(n)).toString()
                                  .substring(1, microPrecision+2)
                                  .replace(/\.?0+$/,'');
  }

});


Object.extend(Object, {

  isDate: function(o) {
    return !!(o && 'getTimezoneOffset' in o);
  }

});


Object.extend(String.prototype, {

  uc: String.prototype.toUpperCase,

  lc: String.prototype.toLowerCase,

  clean: function() {
    return this.strip().replace(/[\r\n\s]+/g, ' ');
  },

  stripStyles: function() {
    return this.replace(new RegExp(Prototype.StyleFragment, 'img'), '');
  },

  stripLinks: function() {
    return this.replace(new RegExp(Prototype.LinkFragment, 'img'), '');
  },

  scrubHTML: function() {
    return this.stripScripts().stripStyles().stripLinks()
                .replace(/\sid="(\S+)"/gi,' id="scrubbed-$1"');
  },

  scrubHTMLBody: function() {
    return this.scrubHTML()
      .replace(/^.*<body[^>]*>/i,'').replace(/<\/body.*$/i,'');
  },

  toInteger: function() {
    return parseInt( this.replace(/[^0-9-]+/g, ''), 10 ) || 0;
  },

  // FIXME: be locale agnostic!
  toFloat: function() {
    return parseFloat( this.replace(/[^0-9.-]+/g, ''), 10 ) || 0;
  },

  toSeconds: (function(){ // this = "weeks:days:hours:minutes:seconds.microseconds"
    var mm = '', i = Date.durationMultipliers.length;
    while (--i) mm += '(?:\\d+:)?';
    var re = new RegExp('^\\.\\d+$|^' + mm + '\\d+(?:\\.\\d+)?$');
    return function() {
      var s = 0;
      if (!re.test(this)) return s;
      var p = this.split(':').reverse(), i = p.length;
      while (i--) s += p[i] * Date.durationMultipliers[i];
      return s;
    };
  })()

});


Element.addMethods({
  xselect: function(element, xpath) {
    return document._getElementsByXPath(xpath, element);
  }
});


Element.addMethods(['TD', 'TH'], {
  realCellIndex: function(el){
    var idx = el.cellIndex;
    el.previousSiblings().filter(function(el){
      var tag = el.tagName.uc();
      return tag === 'TD' || tag === 'TH'
    }).each(function(td){
      if (td.hasAttribute('colspan'))
        idx += parseInt(td.getAttribute('colspan'),10) - 1;
    });
    return idx;
  }
});


$x = document._getElementsByXPath;


// extending http://prototype-window.xilinus.com/ v1.3
if (typeof Window != 'undefined') {
  Object.extend(Window.prototype, {
    updateSize: function() {
      var w = 5, h = 5;
      this.content.immediateDescendants().each(function(el) {
        w += parseInt(el.getWidth(),  10);
        h += parseInt(el.getHeight(), 10);
      });
      this.setSize(w, h, false);
      // bullshit, won't take care of the scrollbars :(
      // this.setSize(this.content.scrollWidth, this.content.scrollHeight, false);
      return this;
    },
    // FIXME: make it fit inside the viewport
    updatePosition: function() {
    }
  });
}

