/*
  $Id: sheet.js,v 1.15 2007/06/11 08:50:07 altblue Exp $
 
  Sheet - Yet Another Sortable Table class (on steroids) ;-)

  (c) 2006 Marius Feraru <altblue@n0i.net>

  This module is free software; you can redistribute it
  and/or modify it under the same terms as Perl itself.
*/

var Sheet = Class.create();

Sheet.prototype = {
  initialize: function (table, options) {
    table = $(table);
    if (!table) return;
    if (table._sheet) return table._sheet;

    options = options || {};

    table._sheet   = this;
    this.table     = table;
    if (!table.id) table.id = 'sheet' + parseInt(new Date() / 1);
    this.sorters   = {};
    this.compTotal = [];
    this.pageSize  = options.pageSize || table.tBodies[0].rows.length;
    this.setCache();
    this.prepTable();
    this.setSortables();
    if ( this.compTotal.length > 0 ) {
      this.prepTFoot();
      this.compTotals();
    }
    this.prepPager();
  },

  sortBy: function(name, desc) {
    var c = $H(this.sorters).find(function(p){return p.value.name === name});
    if (c) {
      c = c.value;
      this.sortMe(false, c.colIdx, c.th, desc);
    }
  },

  setCache: function () {
    var tid = this.table.id;
    if (!document._sheetCache) document._sheetCache = {};
    if (document._sheetCache[tid]) return;
    document._sheetCache[tid] = { sorting: -1, desc: false };
  },

  cache: function () {
    var args = $A(arguments), key = args.shift();
    var cache = document._sheetCache[this.table.id];
    if (!key) return cache;
    if (args.length > 0) {
      cache[key] = args.shift();
    }
    return cache[key];
  },

  prepTable: function() {
    /* No THEAD?!! This sucks :( */
    if (!this.table.tHead) {
      var thead = this.table.createTHead();
      var hrow  = thead.insertRow(-1);
      var frow  = this.table.tBodies[0].rows[0];
      var cols  = frow.cells.length;
      for (var i = 0; i < cols; i++) {
        var th = $T('th', hrow, {
          className: 'sortable sortable-' + this.guessType( $(frow.cells[i]).getInnerText() ),
          innerHTML: 'col #' + (i+1)
        });
      }
    }

    /* fix pages' size if necessary */
    var toFix = 0;
    for (var i = this.table.tBodies.length - 1; i >= 0; i--)
      if (this.table.tBodies[i].rows.length > this.pageSize)
        toFix++;
    if (!toFix) return;

    /* hell, redraw all TBODY(ies) */
    var rows = [];
    for (var i = this.table.tBodies.length - 1; i >= 0; i--) {
      var tbody = this.table.tBodies[i];
      for (var j = tbody.rows.length - 1; j >= 0; j--) {
        var tr = tbody.rows[j];
        rows.push( tbody.removeChild(tr) );
      }
    }
    var curTB = 0, buffer = 0;
    for (var i = rows.length - 1; i >= 0; i--) {
      var tr = rows[i];
      var ctb = this.table.tBodies[curTB] || $T('tbody', this.table);
      ctb.appendChild(tr);
      if (++buffer >= this.pageSize) buffer = 0, curTB++;
    }

  },

  /* play dumb, don't waste to much time here */
  guessType: function(text) {
    if (/\d[-/]\d/.test(text)) return 'Date';
    if (/\d:\d/.test(text)) return 'Duration';
    if (/\d\.\d+\.\d/.test(text)) return 'IP';
    if (/\d\s*[kKmMgG][bB]?/.test(text)) return 'Size';
    if (/\d/.test(text)) return 'Number';
    return 'Text';
  },

  setSortables: function () {
    var toSortOn = this.cache('sorting');
    var hRows = this.table.tHead.rows;
    var thead = {};
    for (var i = 0; i < hRows.length; i++) {
        thead[i] = [];
    }
    for (var i = 0; i < hRows.length; i++) {
      var ccol = 0;
      for (var j = 0; j <= thead[i].length; j++) {
        ccol = j;
        if ( typeof thead[i][j] == 'undefined' ) break;
      }
      for (var t = 0; t < hRows[i].cells.length; t++) {
        var th = $(hRows[i].cells[t]);
        for (var j = 0; j < th.colSpan; j++) {
          thead[i][ccol + j] = 1;
          for (var k = 1; k < th.rowSpan; k++) {
            thead[i+k][ccol + j] = 1;
          }
        }
        if ( th.hasClassName('sortable') ) {
          if ( th.hasClassName('comp-total') ) {
            this.compTotal.push(ccol);
          }
          this.sorters[ccol] = {
            name:     th.getInnerText(),
            th:       th,
            colIdx:   ccol,
            hasTotal: th.hasClassName('comp-total'),
            type:     th.className.replace(/^.*?\bsortable-(\S+).*$/, '$1')
          };
          this.setSorter( th, ccol );
          if (toSortOn >= 0) {
            if (toSortOn == ccol) {
              this.sortMe( false, ccol, th, this.cache('desc') );
            }
          } else if ( th.hasClassName('sort-onload') ) {
            this.sortMe( false, ccol, th, false );
          }
        }
        ccol += parseInt(th.colSpan);
      }
    }
    this.columns = thead[0].length;
  },

  prepTFoot: function () {
    var tfoot = this.table.createTFoot();
    if (tfoot.rows.length < 1) {
      var row = tfoot.insertRow(-1);
      for (var j = 0 ; j < this.columns ; j++) {
        var td = row.insertCell(-1);
        td.className = this.table.tBodies[0].rows[0].cells[j].className;
      }
    }
  },

  prepPager: function() {
    var pages = this.table.tBodies.length;
    if (pages < 2) return;
    var tfoot = this.table.createTFoot();
    var row = tfoot.insertRow(-1);
    row.className = 'pager';
    var td = row.insertCell(-1);
    td.colSpan = this.columns;
    this.prevPageButton = $T('input', td, {type: 'button', value: '< prev'}
      ).observe('click', function() { this.changePage(this.page - 1); }.bind(this));
    td.appendChild( $TT(' Page ') );
    this.currentPageLabel = $T('span', td);
    td.appendChild( $TT(' of ' + pages + '. ') );
    this.nextPageButton = $T('input', td, {type: 'button', value: 'next >'}
      ).observe('click', function() { this.changePage(this.page + 1); }.bind(this));
    this.changePage(0);
  },

  changePage: function(page) {
    if (this.table.tBodies.length < 2) return;
    page = parseInt(page);
    if (isNaN(page) || page < 0) return;
    var pages = this.table.tBodies.length;
    if (page >= pages) return;
    this.page = page;
    for (var i = pages - 1; i >= 0; i--)
      this.table.tBodies[i].style.display = i === page ? '' : 'none';
    this.prevPageButton.disabled = page === 0 ? 'disabled' : '';
    this.nextPageButton.disabled = page < pages -1 ? '' : 'disabled';
    this.currentPageLabel.innerHTML = page + 1;
  },

  compTotals: function (colIdx) {
    var columns = isNaN(colIdx) ? this.compTotal : [ colIdx ];
    columns.each(function(c){
      var td = $(this.table.tFoot.rows[0].cells[c]);
      td._svalue = 0;
      td.innerHTML = '…';
      td.addClassName('Sorting-pending');
      setTimeout(this.computeTotals.bind(this, c), 10);
    }.bind(this));
  },

  computeTotals: function (c) {
    var td = $(this.table.tFoot.rows[0].cells[c]);
    $A(this.table.tBodies).each( function(tbody) {
      $A(tbody.rows).each(function(row){
        td._svalue += this.getValue(c, row);
      }.bind(this));
    }.bind(this));
    td.innerHTML = this[ 'valueTo' + this.sorters[c].type ](td._svalue);
    td.removeClassName('Sorting-pending');
  },

  setSorter: function (th, colIdx) {
    th.observe('click', this.sortMe.bindAsEventListener(this, colIdx, th, false));
  },

  clearSorting: function () {
    $H(this.sorters).extract('th').invoke('removeClassName', 'fwdSort', 'revSort');
    this.table.select('.Sorting').invoke('removeClassName', 'Sorting');
  },

  sortMe: function (ev, colIdx, th, desc) {
    if (ev) {
      if (!th) th = Event.element(ev);
      Event.stop(ev);
    }
    if (this.isSorting) return false;
    this.isSorting = true;
    th.addClassName('Sorting-pending');
    var isrev = th.hasClassName('Sorting');
    th.removeClassName('Sorting');
    this.cache('sorting', colIdx);
    this.cache('desc', desc || isrev);
    if (desc && !isrev) {
      setTimeout(function (th,colIdx) {
        this.sortColumn(th, colIdx);
        this.sortColumn(th, colIdx, true);
      }.bind(this, th, colIdx), 10);
    } else {
      setTimeout(this.sortColumn.bind(this, th, colIdx, isrev), 10);
    }
  },

  sortColumn: function (th, colIdx, isrev) {
    if (!isrev) this.clearSorting();
    var fwsort = th.hasClassName('fwdSort') ? false : true;
    if (fwsort) {
      th.removeClassName('revSort').addClassName('fwdSort');
    } else {
      th.addClassName('revSort').removeClassName('fwdSort');
    }
    var rows = [];
    for (var i = this.table.tBodies.length - 1; i >= 0; i--) {
      var tbody = this.table.tBodies[i];
      for (var j = tbody.rows.length - 1; j >= 0; j--) {
        var tr = tbody.rows[j];
        if (!isrev) $(tr.cells[colIdx]).addClassName('Sorting');
        rows.push( tbody.removeChild(tr) );
      }
    }
    if (!isrev) rows = rows.sortBy( this.getValue.bind(this, colIdx) );
    var idx = 1, curTB = 0, buffer = 0;
    for (var i = 0, l = rows.length; i < l; i++) {
      var tr = rows[i];
      tr.removeClassName(idx % 2 ? 'even' : 'odd');
      tr.addClassName(idx++ % 2 ? 'odd' : 'even');
      this.table.tBodies[curTB].appendChild(tr);
      if (++buffer >= this.pageSize) buffer = 0, curTB++;
    }
    th.removeClassName('Sorting-pending').addClassName('Sorting');
    this.isSorting = false;
    this.changePage(0);
  },

  getValue: function (colIdx, tr) {
    var col = tr[tr.cells.length > 0 ? 'cells' : 'childNodes'][colIdx]; /* Yes, another IE7 fuckup */
    if (typeof col._svalue != 'undefined') {
      return col._svalue;
    }
    var val = $(col).getInnerText().lc();
    try { val = this['valueFor' + this.sorters[colIdx].type ](val) } catch (e) {};
    col._svalue = val;
    return val;
  },

  valueForNumber: function (s) {
    s = s.replace(/[, ]/g, '');
    var number = /(-?\d+(?:\.\d+)?)/.exec(s);
    return number ? Number(number[0]).valueOf() : Number('-1');
  },

  valueToNumber: function (v) {
    return v.toFixed(2);
  },

  valueForDate: function (s) {
    var dt = s.split(/\s+/);
    var nd = dt[0].replace(
                /^(\d\d\d\d)[-/](\d\d)[-/](\d\d)$/, '$1$2$3'
              ).replace(
                  /^(\d\d)[-/](\d\d)[-/](\d\d\d\d)$/, '$2$1$2'
                );
    if (dt.length > 1) {
      nd += dt[1].replace(
                /^(\d\d):(\d\d):(\d\d)$/, '$1$2$3'
              ).replace(
                  /^(\d\d):(\d\d)$/, '$1$200'
                );
    } else {
      nd += '000000';
    }
    nd = Number(nd).valueOf();
    return isNaN(nd) ? -1 : nd;
  },

  valueToDate: function (v) {
    return String(v).replace(/^(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/, '$1-$2-$3 $4:$5:$6');
  },

  valueForIP: function (s) {
    return Number(s.split('.').invoke('sprintf', '%03d').join('')).valueOf();
  },

  valueToIP: function (v) {
    return String(v).replace(/^(\d\d\d)(\d\d\d)(\d\d\d)(\d\d\d)$/, '$1.$2.$3.$4'
      ).replace(/(^|\.)0+(\d)/g, '$1$2');
  },

  valueForSize: function (s) {
    var p = /((?:[0-9]*\.)?[0-9]+(?:[eE][-+]?[0-9]+)?)\s*([a-z])?b?/.exec(s);
    if (!p) return -1;
    var num = Number(p[1]).valueOf();
    if ( isNaN(num) ) return -1;
    var prefix = { K: 10, M: 20, G: 30, T: 40, P: 50, E: 60, Z: 70, Y: 80 };
	return num * Math.pow( 2, prefix[ (p[2]||'').uc() ] || 0 );
  },

  valueToSize: function (v) {
    var mag = 'B', mags = ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
    while (v > 1023 && mags.length > 0) v /= 1024, mag = mags.shift();
    return v.toFixed(2) + ' ' + mag;
  },

  valueForDuration: function (s) {
    var p = s.split(':'), d = 0, i = 1;
    while (p.length > 0) {
      var l = Number(p.pop()).valueOf();
      if ( isNaN(l) ) return -1;
      d += i * l;
      i *= 60;
    }
    return d;
  },

  valueToDuration: function (s) {
    var m = parseInt( s / 60 ); s = s % 60;
    var h = parseInt( m / 60 ); m = m % 60;
    return [h,m,s].invoke('sprintf', '%02d').join(':');
  }

};

Neo.onPageLoad( function () {
  $$('table.sortable').each(function (table) {
    new Sheet(table);
  });
});
