function TableSorter(tableId, sortOrder, callback) {
  this.tableId = tableId;
  this.sortOrder = sortOrder;
  this.rows = null;
  this.sorted = null;
  this.col = -1;
  this.reverse = false;
  // A callback function that will be called after every table sort.
  this.callback = callback;
  // Number in string regexp.
  this.re = /(\d+(?:[.,]\d+)?)(.*)/;
  this.sortCache = new Object();
  this.validCache = false;
  this.savedClickCol = -1;
}

TableSorter.prototype.sortTable = function(col, reverse) {
  if(this.sorted == null) {
    this.createTableCache();
  }

  if(!this.validCache) {
    this.savedClickCol = col;
    return;
  }

  if(col > this.sorted.length) {
    throw "Illegal column index!";
  }

  /*
  if(this.col == col && this.reverse == reverse) {
    return;
  }
  */

  if(this.col == col) {
    this.reverse = !this.reverse;
  } else {
    this.reverse = reverse;
  }

  this.col = col;
  // this.reverse = reverse;
  var table = document.getElementById(this.tableId);
  while(table.firstChild) {
    table.removeChild(table.firstChild);
  }

  var data = this.sorted[col];
  if(this.reverse) {
    for(var i = data.length - 1; i >= 0; i--) {
      table.appendChild(this.rows[data[i].row]);
    }
  } else {
    for(var i = 0; i < data.length; i++) {
      table.appendChild(this.rows[data[i].row]);
    }
  }

  if(this.callback) {
    this.callback(this.col, this.reverse);
  }
};

TableSorter.prototype.createTableCache = function() {
  // Row to text-in-cells mapping.
  var arr = [];
  var table = document.getElementById(this.tableId);
  var rows = table.rows;
  this.rows = [];
  for(var i = 0; i < rows.length; i++) {
    // We'll need to clone these since the references
    // will be removed when we empty the table.
    this.rows.push(rows[i].cloneNode(true));
    var cells = rows[i].cells;
    // Add mapping.
    arr.push({ row: i, text: new Array(cells.length) });
    for(var j = 0; j < cells.length; j++) {
      var str = this.extractText(cells[j]);
      arr[i].text[j] = str;
    }
  }

  var cols = arr[0].text.length;
  this.sorted = new Array(cols);
  var functions = [];
  var self = this;
  var counter = 0;
  for(var i = 0; i < cols; i++) {
    // We must copy the array contents since we'll be
    // using references otherwise.
    // this.sorted[i] = this.sort(i, arr);

    // A hack to prevent Firefox from displaying a
    // warning message when the script takes too long
    // to complete (i.e. when sorting a large amount of data).
    setTimeout(function() {
      self.sorted[counter] = self.sort(counter, arr);
      if(++counter == cols) {
	self.validCache = true;
	if(self.savedClickCol != -1) {
	  self.sortTable(self.savedClickCol, self.reverse);
	  self.savedClickCol = -1;
	}
      }
    }, 3);
  }
};

TableSorter.prototype.sort = function(col, array) {
  var arr = this.arrayCopy(array);
  var me = this;
  var sorter = function(a, b) {
    var index = null;
    for(var i = 0; i < me.sortOrder[col].length; i++) {
      var type = null;
      // Check if it's an array with a type specifier.
      if(typeof(me.sortOrder[col][i].shift) != 'undefined') {
	if(me.sortOrder[col][i].length != 2) {
	  throw("Illegal array length in sort order for col: " + col + " at position: " + i);
	  return;
	}

	index = me.sortOrder[col][i][0];
	type = me.sortOrder[col][i][1];
      } else {
	index = me.sortOrder[col][i];
      }

      var s1 = a.text[index].toLowerCase();
      var s2 = b.text[index].toLowerCase();
      var result = me.cmp(s1, s2, type);
      if(result != 0) {
	return result;
      }
    }

    return 0;
  };

  arr.sort(sorter);
  return arr;
};

/**
 * Get the character code for the specified character 'c'.
 *
 * This method is only valid when comparing strings (i.e. sorting)
 * and will return modified character codes for swedish characters
 * since the Unicode values for those are in the wrong order.
 */
TableSorter.prototype.getCharCode = function(c) {
  var code = c.charCodeAt(0);
  switch(code) {
  case 0xc5:
    return 65600;
  case 0xc4:
    return 65601;
  case 0xd6:
    return 65602;
  case 0xe5:
    return 65603;
  case 0xe4:
    return 65604;
  case 0xf6:
    return 65605;
  default:
    return code;
  }
};

TableSorter.prototype.cmp = function(a, b, type) {
  if(type == 'n') {
    var i = a;
    if(i.length == 0) {
      i = Number.MIN_VALUE;
    } else {
      i = parseFloat(i.replace(/ /g, '').replace(',', '.'));
    }

    var j = b;
    if(j.length == 0) {
      j = Number.MIN_VALUE;
    } else {
      j = parseFloat(j.replace(/ /g, '').replace(',', '.'));
    }

    return i - j;
  }

  if(type != 's') {
    var r1;
    var r2;
    if((r1 = this.re.exec(a)) && (r2 = this.re.exec(b)) && (r1.index == r2.index)) {
      // Both expressions evaluated (i.e. not null) and the indeces for the digits are the same.
      if(a.substring(0, r1.index) == b.substring(0, r2.index)) {
	if(r1[1] != r2[1]) {
	  // The strings before the numbers are the same and the numbers differ.
          var i = r1[1];
          if(i.length == 0) {
            i = Number.MIN_VALUE;
          } else {
            i = parseFloat(i.replace(',', '.'));
          }

          var j = r2[1];
          if(j.length == 0) {
            j = Number.MIN_VALUE;
          } else {
            j = parseFloat(j.replace(',', '.'));
          }

	  return i - j;
	} else {
	  // Both the strings before the numbers, and the numbers are the same. Compare the tails if any.
	  if(r1[2].length != 0 && r2[2].length != 0) {
	    return this.cmp(r1[2], r2[2], type);
	  }
	}
      }
    }
  }

  var len1 = a.length;
  var len2 = b.length;
  var min = Math.min(len1, len2);
  for(var j = 0; j < min; j++) {
    var c1 = this.getCharCode(a.charAt(j));
    var c2 = this.getCharCode(b.charAt(j));
    if(c1 != c2) {
      return c1 - c2;
    }
  }

  return len1 - len2;
};

TableSorter.prototype.extractText = function(cell) {
  var children = cell.childNodes;
  var text = [];
  for(var i = 0; i < children.length; i++) {
    var child = children.item(i);
    if(child.nodeType == 1) {
      if(child.tagName == 'BR') {
	text.push(' ');
      } else {
	text.push(this.extractText(child));
      }
    } else if(child.nodeType == 3) {
      text.push(child.nodeValue);
    }
  }

  var str = text.join('');
  return str.replace(/\s\s+/g, ' ').replace(/^\s*|\s*$/g, '');
};

TableSorter.prototype.arrayCopy = function(array) {
  var copy = new Array(array.length);
  for(var i = 0; i < array.length; i++) {
    copy[i] = array[i];
  }

  return copy;
};
