// ==UserScript==
// @name          BugSort
// @namespace     http://squarefree.com/userscripts
// @description   Lets you sort Bugzilla bug lists without contacting the server.
// @include       https://bugzilla.mozilla.org/buglist.cgi?*
// ==/UserScript==

/*

  Author: Jesse Ruderman - http://www.squarefree.com/

  Version history:
    2005-10-19: Make it work with newer versions of Greasemonkey, which isolate user scripts from the page.
    2005-05-21: Fix JS strict warnings.
    2005-05-02: Faster, see http://www.squarefree.com/2005/05/02/faster-bug-sort-user-script/.
    2005-04-18: Initial release, see http://www.squarefree.com/2005/04/18/bug-sort-user-script/.

  License: MPL
  
  Features:

   * The sort is stable.  For example, if you sort by votes and then sort by resolution, 
     the bugs at the top are open bugs sorted by votes.

   * You can reverse the order of the bugs by clicking a column header again.
   
  On Jesse's computer (1.6 GHz P4), this script takes about 2 seconds to sort a 600-bug list.

*/


/*
 Odd things about Bugzilla:
  * some columns have special sort orders
  * multirow "staggered" headers
  * table isn't split across pages like in most search engines. but it is split into multiple tables.
  * even/odd row coloring
*/

var BUGS_PER_TABLE = 100;

var lastSortedColumn = null; // sorting by the same column twice (or more) in a row reverses the list

function trim(str) {
  return str.replace(/^\s*/, '').replace(/\s*$/, '');
}


function getBugTables()
{
  var i, table, tables=[];
  for (i=0; table = document.getElementsByTagName("table")[i]; ++i) 
    if (table.className == "bz_buglist")
      tables.push(table);
  return tables;
}


function fixHeaderLinks(firstTime)
{
  var t, i, table, row, colIndex, colName, j, cell, link;

  for (t=0; table = bugTables[t]; ++t)
    for (i=0; row = table.rows[i]; ++i)
      if (row.cells[0].tagName=="TH")
        for (colIndex=0, j=0; cell = row.cells[j]; ++j)
        {
          if ( (link = cell.getElementsByTagName("A")[0]) ) {
            colName = trim(cell.textContent);
         
            //if (colIndex != lastSortedColumn)
              link.href = "javascript: // Sort by " + colName;
            //else
            //  link.href = "javascript: // Reverse sort order"; 

            if (firstTime)
              addClickHandler(link, colIndex, colName);
          }
          colIndex += cell.colSpan;
        }
}

function addClickHandler(link, colIndex, colName)
{
  // This function exists (is not inline code) so that colIndex, colName, and reversed live in the appropriate closure.

  link.addEventListener("click", function() { sortBugs(colIndex, colName); }, false);
}

// getSortMethod : given a column name, return two functions:
// - preprocess
// - compare
function getSortMethod(columnName)
{
  function compareNumericalDesc(x,y) { return y-x; }
  function compareNumericalAsc(x,y) { return x-y; }
  function compareString(x,y) { return (x==y) ? 0 : (y<x) ? 1 : -1; }
  
  function preprocessUsingHash(hash) { return function(cellText) { return hash[cellText]; }; }
  function integerize(cellText) { return parseInt(cellText, 10); }
  function uppercase(cellText) { return cellText.toUpperCase(); }

  var hashes = 
    {
      "Sev" : { "blo":7, "cri":6, "maj":5, "nor":4, "min":3, "tri":2, "enh":1 }, // BMO's sane order
      "Status" : { "UNCO":7, "NEW":6, "ASSI":5, "REOP":4, "RESO":3, "VERI":2, "CLOS":1 }, // BMO's sane order
      "Resolution": { "":7, "FIXE":6, "INVA":5, "WONT":4, "DUPL":3, "WORK":2, "MOVE":1 } // BMO's weird order
    };


  switch(columnName) {

    case "Sev":
    case "Status":
    case "Resolution":
      return { preprocess: preprocessUsingHash(hashes[columnName]), compare: compareNumericalDesc };

    case "Votes":
      // bugs with the most votes first
      return { preprocess: integerize, compare: compareNumericalDesc };
      
    case "ID":
      // oldest bugs first
      return { preprocess: integerize, compare: compareNumericalAsc };

    case "Opened":
    case "Changed":
      return { preprocess: integerize, compare: compareNumericalAsc };

    default:
      // case-insensitive, alphabetical
      return { preprocess: uppercase, compare: compareString};

  }
}



function sortBugs(columnIndex, columnName)
{
  var sortMethod = getSortMethod(columnName);
  
  var i, table, j, row;
  var rows = [];
  var cellText;

  //var t1 = new Date();

  // gather the rows into an array to be sorted.
  for (i=0; table = bugTables[i]; ++i) {
    for (j=0; row = table.rows[j]; ++j) {
      if (row.cells[0].tagName == "TD") {
        // Put a 4-element array into the array "rows".
        // For time fields, real data can be hidden in the title
        var data = row.cells[columnIndex].getAttribute("title") ||
                   trim(row.cells[columnIndex].textContent);
        
        rows.push(
          [
            sortMethod.preprocess(data) ,
            j,     // make it a stable sort by keeping the old row index around
            row,   // not compared, but kept with the sorted objects
            parseInt(trim(row.cells[0].textContent), 10) // bug number
          ]
        );
      }
    }
  }

  //var t2 = new Date();

  if (lastSortedColumn == columnIndex) {
    rows.reverse(); // only reverse the order of the rows; don't sort at all!
  }
  else {
    var compare = sortMethod.compare;

    function compareAll(x,y) 
    {
      var c;
      if ((c = compare(x[0], y[0])) != 0) // if primary sort column differs
        return c;
      return x[1] - y[1]; // stable sort. these always differ for different rows.
    }
    
    // performance: This is just data, no DOM, so it's really fast compared to the other parts of the script.
    rows.sort(compareAll);
    
    var numbers = new Array();
    
    for (i = 0; i < rows.length; i++)
    {
      numbers.push(rows[i][3]);
    }
    
    var buglist = numbers.join("%3A");    
    document.cookie = 'BUGLIST=' + buglist + '; expires=Thu, 2 Aug 2012 20:47:11 UTC; path=/';
  }

  //var t3 = new Date();
  
  // Remove all the tables from the page.  They will be put back one at a time.
  // This helps a *lot* with speed, since most of the time of this script is spent in DOM and most of that
  // time is spent in changing the DOM / layout.
    

  for (i=0; table = bugTables[i]; ++i) {
    // Keep track of where the first bug table was so we can put it back.
    var parent = table.parentNode;
    var nextSibling = table.nextSibling; // this is a text node that survives the subsequent removal!

    // Remove it from the document (speeds things up)
    //parent.removeChild(table);

    // Later, add this table to the document with the correct rows
    
    function thingie(table, parent, nextSibling, i) {
      var j, row, rowTuple;

      var tableBody = table.tBodies[0];
      
      // Clear data rows, which might belong in a subsequent table.
      for (j = 0; row = tableBody.rows[j]; ++j)
      {
        if (row.cells[0].tagName == "TD") {
          --j;
          tableBody.removeChild(row);      
        }
      }
      
      
      // Add the children that belong here.
  
      // Toss the rows into tables, one tablefull (BUGS_PER_TABLE) at a time.
      // Unfortunately, DOM 2 HTML insertRow is for creating *new* rows, so use DOM 2 Core.
  
      for (j = i * BUGS_PER_TABLE; (j < (i+1) * BUGS_PER_TABLE) && (rowTuple = rows[j]); ++j) {
        row = rowTuple[2];
        
        if (j % 2)
          row.className = row.className.replace(/bz_odd/, "bz_even");
        else
          row.className = row.className.replace(/bz_even/, "bz_odd");
  
        tableBody.appendChild(row);
      }
   
      // Put it back into the document.    
      //parent.insertBefore(table, nextSibling);
    }
    
    setTimeout(thingie, i * 200 + 50, table, parent, nextSibling, i);

 }
        
  lastSortedColumn = columnIndex;
  //fixHeaderLinks(false);


  //var t4 = new Date();
  //GM_log((t4 - t3) + ", " + (t3 - t2) + ", " + (t2 - t1));
}

var bugTables = getBugTables();

window.sortBugs = sortBugs;

if (bugTables.length) { // false when Greasemonkey inserts this script into the "please stand by" page, etc.
  fixHeaderLinks(true);
}
