// hnsort
// v0.1
// Copyright (c) 2009, Wayne Burkett 
// Released under the GPL license
// http://www.gnu.org/copyleft/gpl.html

// ==UserScript==
// @name           hnsort
// @namespace      http://wayneburkett.com
// @description    Sort articles on the Hacker News homepage
// @include        http://news.ycombinator.com/news
// @include        http://news.ycombinator.com/
// ==/UserScript==

function $(id) {
    return document.getElementById(id);
}

// returns an array of "story" meta-objects
function getStories(tbody) {
    if (getStories._vals)
        return getStories._vals;
    var stories = document.evaluate("id('articlesTable')//tr[td[@class='title']]", tbody, null,     
            XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
    var triplets = [];
    var length = stories.snapshotLength;
    for (var i = 0; i < length - 1; i++) {
        var headline = stories.snapshotItem(i);
        var subtext = headline.nextSibling;
        if (subtext) {
            addSubtextHooks(subtext);
            triplets.push(createMetaObj(headline, i, length));
        }
    }
    getStories._vals = triplets;
    return getStories._vals;
}    

// creates a meta-object with details for the given 
// headline; used later to determine sort order
function createMetaObj(headline, pos, length) {
    var subtext = headline.nextSibling;
    var res = subtext.textContent.split(/\s+/); 
    return {
        elements: [headline, subtext, subtext.nextSibling],
        rank: length - pos,
        points: parseInt(res[0]),
        comments: parseInt(res[8]) || 0,
        age: (new Date()).getTime() - 
            (parseInt(res[4]) * getMultiplier(res[5]))
    }
}

function getMultiplier(unit) {
    var mult = 1;
    switch (unit) {
        case "day":
        case "days":
            mult *= 24;
        case "hour":
        case "hours":
            mult *= 60;
        case "minute":
        case "minutes":
            mult *= 60;
        case "second":
        case "seconds":
            mult *= 1000;
    }
    return mult;
}


// wraps the "n hours ago" text in a span and gives it a class 
// name so that it's easier to select later
function addSubtextHooks(subtext) {
    var userLink = subtext.getElementsByTagName("a")[0];
    var ageStr = userLink.nextSibling;
    wrapTextNode(splitTextNode(ageStr, ageStr.nodeValue.indexOf("|")), "age");
}

// wraps the given text node with a <span>
function wrapTextNode(node, clazz) {
    var el = document.createElement("span");
    var clone = node.cloneNode(true);
    el.className = clazz;
    el.appendChild(clone);
    node.parentNode.replaceChild(el, node);
    el.replaceChild(node, clone);
    return clone;
}

// splits a text node in two at the specified offset
// and returns a reference to the first half (which
// is the original node)
function splitTextNode(node, offset) {
    var val = node.nodeValue; 
    var txt = document.createTextNode(val.substring(offset, val.length));
    node.nodeValue = val.substring(0, offset);
    node.parentNode.insertBefore(txt, node.nextSibling);
    return node;
}

// marks the given sort link as selected and unmarks its siblings
function updateSelected(link) {
    var sibs = link.parentNode.childNodes;
    for (var i = 0; i < sibs.length; i++) {
        if (sibs[i].nodeType != sibs[i].ELEMENT_NODE)
            continue;
        sibs[i].style.fontWeight = (sibs[i] == link) ? "bold" : "normal";
    }
}

// generates an array of CSS rules
function genStyles(selectors, name) {
    var rules = [];
    for (var sel in selectors) {
        rules.push(selectors[sel] + "{color:" + 
            (sel === name ? "red" : "inherit") + " !important;}")
    }
    return rules.join("\n");
}

// applies latest styles (based on which sort link is selected)
function updateStyles(selected) {
    var selectors = {
        points: "#articlesTable .subtext span:first-child",
        age: "#articlesTable .subtext .age",
        comments: "#articlesTable .subtext a:last-child"
    };
    var styles = genStyles(selectors, selected);
    $("hnsort").innerHTML = styles;
}

function createSortLink(text, sortKey) {
    var link = document.createElement("a");
    link.appendChild(document.createTextNode(text));
    link.href = "#";
    link.addEventListener("click", function(e) { 
        var tbody = $("articlesTable").firstChild;
        sort(tbody, (sortKey || text), (link.style.fontWeight == "bold"));
        updateSelected(link);
        updateStyles(text);
        return false;
    }, false);
    return link;
}

function sort(tbody, sortKey, sorted) {
    var refEl = tbody.lastChild.previousSibling;
    console.log(refEl);
    (function(stories) {
        return sorted ? stories.reverse() : 
            stories.sort(function(a, b) { 
                return b[sortKey] - a[sortKey] 
            });
    })(getStories(tbody)).forEach(function(el) {
        el.elements.forEach(function(item) { 
            tbody.insertBefore(item, refEl)
        });
    });
}

// adds an empty style element to the head of the document 
// that can be later retrieved by ID and updated
function addStyleElement() {
    var head = document.getElementsByTagName('head')[0];
    var style = document.createElement('style');
    style.type = 'text/css';
    style.id = "hnsort";
    head.appendChild(style);
}

function insertControls() {
    var tbody = document.getElementsByTagName("tbody")[0];
    var tr = tbody.firstChild.nextSibling;
    var articlesCell = tbody.childNodes[2].firstChild;
    var articlesTable = articlesCell.firstChild;
    var linksWrapper = document.createElement("p"); 
    linksWrapper.id = "sortLinks";
    linksWrapper.className = "title";
    articlesTable.id = "articlesTable";
    var links = [
        createSortLink("#", "rank"),
        createSortLink("points"),
        createSortLink("age"),
        createSortLink("comments")];
    links[0].style.fontWeight = "bold";
    linksWrapper.appendChild(document.createTextNode("\u00a0\u00a0Sort by "));
    for (var i = 0; i < links.length; i++) {
        if (i != 0)
            linksWrapper.appendChild(document.createTextNode(" | "));
        linksWrapper.appendChild(links[i]);
    }
    tr.appendChild(linksWrapper);
}

window.addEventListener("load", function() {
    insertControls();
    addStyleElement();
}, false);

// 2009-08-12 - 0.2 - now plays nicely with the "Hacker News Toolkit"
// 2009-08-12 - 0.1 - released
