// ==UserScript==
// @name                Forum wrangler
// @version             2.0
// @date                2008-12-10
// @author              Ian Malpass ( ian AT etsyhacks DOT com )
// @namespace           etsy.com
// @description         Keeps track of your forum usage and adds features to forum pages
// @include             http://www.etsy.com/*
// ==/UserScript==

// capture logout events
decorateLogout();

// find out which user I am
var username = whoami();

// configure field prefixes - use usernames to separate data for multiple users using the same browser
var statePrefix = username + '_state_';
var posPrefix = username + '_pos_';
var lastReadPagePrefix = username + '_lastReadPage_';
var lastReadOnPrefix = username + '_lastReadOn_';
var postCountPrefix = username + '_postCount_';
var postsPrefix = username + '_postedOnPage_';
var forumPrefix = 'forum_';

// configure the colours we'll use for highlighting the threads
var colours = {
    posted: '#fbeaea',
    postedNew: '#ffd6d6',
    read: '#f2fbe4',
    readNew: '#ddecc4',
    started: '#ffa3a3'
};    

// useful pattern we'll use a lot
var extractThread = /thread_id=(\d+)/;

if ( username != null && username != "" && document.location.href.indexOf( 'forums' > -1 ) ) {
    // if we're in the forums, and we know who we are, do the real forum wrangling
    // if we're not in the forums, we're just capturing logouts

    if ( document.location.href.match( 'forums_main.php' ) ) {
        decorateForumList()
    } else if ( document.location.href.match( 'forums_board.php' ) ) {
        decorateForumList()
    } else if ( document.location.href.match( 'forums_search.php' ) ) {
        decorateForumList()
    } else if ( document.location.href.match( 'forums_thread.php' ) ) {
        checkPosted(); // have I just posted?
        addFormHandler(); // make sure we capture posts
        captureForumPosition(); // and track where we are
    } else if ( document.location.href.match( 'forums_user_threads.php' ) ) {
        // don't use coloured highlights - all these are posted-in
        decorateForumList( { noPostedHighlight: true } );
    }
}

// we set a flag when we post - we need to check to see if the flag is set 
// and if the post was successful.
function checkPosted () {
    var justPosted = GM_getValue( 'justPosted' );
    if ( justPosted == null || justPosted == "" ) return; // no post
    GM_setValue( 'justPosted', "" );
    var redText = getElementsByClassName( 'red_text' ); // seek errors
    var postedOK = true;
    for ( var r = 0; r < redText.length; r++ ) {
        if ( redText[ r ].firstChild && redText[ r ].firstChild.data && redText[ r ].firstChild.data.indexOf( 'Your post contained no text' ) > -1 ) {
            // we found the error text - post failed
            postedOK = false;
        }
    }
    if ( postedOK ) {
        // post succeeded
        var bits = document.location.search.substring( 1 ).split( '&' );
        // find out where we are so we can flag the thread and move to the last page we were on
        var data = {};
        for ( var b = 0; b < bits.length; b++ ) {
            var vals = bits[ b ].split( '=' );
            data[ vals[ 0 ] ] = vals[ 1 ];
        }
        // set status
        GM_setValue( statePrefix + data.thread_id, 'posted' );
        var page = GM_getValue( lastReadPagePrefix + data.thread_id );
        if ( page != null && Number( page ) > 1 ) {
            // redirect to the last page we were on
            document.location.href = document.location.href + '&page=' + page;
        }
    }
}

// set a flag when we post so checkPosted() knows to do its work
function addFormHandler () {
    if ( document.forms[ 1 ] ) {
        document.forms[ 1 ].addEventListener( "submit", function () { GM_setValue( 'justPosted', "true" ) }, true );
    }
}

// where am I?
function captureForumPosition () {
    var bits = document.location.search.substring( 1 ).split( '&' );
    // parse the query string to find the thread ID and page
    var data = {};
    for ( var b = 0; b < bits.length; b++ ) {
        var vals = bits[ b ].split( '=' );
        data[ vals[ 0 ] ] = vals[ 1 ];
    }
    if ( data.thread_id == null ) return;
    data.page = ( data.page == null ) ? 1 : Number( data.page );

    // configure field names for storing data
    var posField = posPrefix + data.thread_id;
    var lastReadPageField = lastReadPagePrefix + data.thread_id;
    var stateField = statePrefix + data.thread_id;
    var lastReadOnField = lastReadOnPrefix + data.thread_id;
    var postCountField = postCountPrefix + data.thread_id;

    var lastPage = GM_getValue( posField );
    lastPage = ( lastPage == null ) ? 0 : Number( lastPage );
    GM_setValue( lastReadPageField, data.page ); // where was I last
    if ( lastPage == null || lastPage < data.page ) {
        GM_setValue( posField , data.page ); // what's the latest page in the the thread that I've read?
    }
    var state = GM_getValue( stateField );
    if ( state == null ) {
        GM_setValue( stateField, 'read' ); // First time reading this thread
    }
    var now = new Date;
    GM_setValue( lastReadOnField, String( now.valueOf() ) );

    // find a store which forum this thread is in - for future use
    var getForumId = /forums_board.php\?forum_id=(\d+)/;
    var links = document.getElementsByTagName( 'a' );
    for ( var l = 0; l < links.length; l++ ) {
        var link = links[ l ];
        if ( link.href ) {
            var match = getForumId.exec( link.href );
            if ( match ) {
                GM_setValue( forumPrefix + data.thread_id, match[ 1 ] );
            } else if ( link.href.indexOf( 'forums_thread.php' ) > -1 ) {
                // capture how many posts are in the thread, so we know if there are new posts later
                var postCount = String( link.parentNode.parentNode.cells[ 0 ].innerHTML.match( /\d+/ ) );
                break;
            }
        }
    }

    // check the page for old posts by this user
    var isForumLink = /post-(\d+)/;
    for ( l; l < links.length; l++ ) {
        var link = links[ l ];
        if ( ! link.name ) continue;
        var match = isForumLink.exec( link.name );
        if ( ! match ) continue;
        // seek the shop link
        l += 2;
        while ( links[ l ] != null && ( links[ l ].href == null || links[ l ].href.indexOf( 'shop.php' ) == -1 ) ) l++;
        // Get the user name from the shop link
        if ( links[ l ].innerHTML == username ) {
            // this was by me - make sure this thread's state is "posted"
            GM_setValue( stateField, 'posted' );
            // and add it to the list of posts for this thread
            insertPage( data.thread_id, data.page );
            break;
        }
    }
    GM_setValue( postCountField, postCount );
}

// this does all the highlighting and adding-of-links
function decorateForumList( args ) {
    if ( args == null ) args = {};
    var links = document.getElementsByTagName( 'a' );
    // loop through all the links, looking for thread links
    for ( var l = 0; l < links.length; l++ ) {
        var link = links[ l ];
            
        if ( link.href && link.href.indexOf( 'forums_thread.php' ) > -1 ) {
            var match = extractThread.exec( link.href );
            var thread_id = match[ 1 ];

            // find out what we know about this thread
            var page  = GM_getValue( posPrefix + thread_id );
            var state = GM_getValue( statePrefix + thread_id );

            // get the parent row node
            var rowNode = link.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode
            var count = Number( rowNode.cells[ 2 ].innerHTML ); // how many posts?
            var lastCount = GM_getValue( postCountPrefix + thread_id );
            if ( lastCount != null && lastCount != count ) {
                // new posts - make the row bold and get the right state colour
                state += 'New';
                rowNode.style.fontWeight = 'bold';
            }
            if ( state != null && colours[ state ] != null && ! args.noPostedHighlight ) {
                // we have a state, and we want coloured highlights - set the background colour
                rowNode.style.backgroundColor = colours[ state ];
            }
            // calculate the last page
            var lastPage = Math.floor( ( count - 1 ) / 10 ) + 1;

            // build the "jump to" links
            // we do some complicated stuff to work out if another hack has created some infrastructure for us already
            var nodes = getElementsByClassName( 'fwJumpTo', rowNode );
            var div;
            if ( nodes.length == 0 ) {
                // no "jump to" box already - make one
                div = document.createElement( 'div' );
                // format the div
                div.className = 'fwJumpTo dark_grey_text';
                div.style.width = '100%';
                div.style.borderTop = '1px solid rgb(218, 219, 214)';
                div.style.marginTop = '2px';
                div.style.paddingTop = '2px';
                div.style.fontSize = '10px';
                // add it to the row
                rowNode.cells[ 3 ].appendChild( div );
            } else {
                // stash a reference to the existing "jump to" box
                div = nodes[ 0 ];
            }

            var threadLinks = [];
            if ( page != null ) {
                // we've read this thread - link to the last read page
                var a = document.createElement( 'a' );
                a.href = 'forums_thread.php?thread_id=' + thread_id + '&page=' + page;
                a.innerHTML = "last read";
                threadLinks.push( a );
            }
            // link to the last page
            var a = document.createElement( 'a' );
            a.href = 'forums_thread.php?thread_id=' + thread_id + '&page=' + lastPage;
            a.innerHTML = "last page";
            threadLinks.push( a );

            // do we need a "jump to post" link?
            var state = GM_getValue( statePrefix + thread_id );
            var postsLink;
            var linksDiv;
            var lIncrement = 2;
            var nodes = getElementsByClassName( 'postsLink', rowNode );
            if ( nodes.length ) {
                // make sure we skip over the extra link when we finish this loop iteration
                lIncrement++;
            }
            if ( state && state == 'posted' ) {
                if ( nodes.length ) {
                    // another hack has already added a "jump to post" link, just stash the references
                    postsLink = nodes[ 0 ];
                    var divs = getElementsByClassName( 'linksDiv', rowNode );
                    linksDiv = divs[ 0 ];
                    linksDiv.appendChild( document.createElement( 'br' ) ); // line break between existing links and our new ones
                } else {
                    // create the "jump to post" link 
                    postsLink = document.createElement( 'a' );
                    postsLink.className = 'postsLink';
                    postsLink.innerHTML = "post";
                    postsLink.style.cursor = 'pointer';
                    postsLink.href = "#";
                    threadLinks.push( postsLink );
                    // and associated div for page links
                    linksDiv = document.createElement( 'div' );
                    linksDiv.className = 'dark_grey_text linksDiv';
                    linksDiv.style.width = '100%';
                    linksDiv.style.marginTop = '2px';
                    linksDiv.style.paddingTop = '2px';
                    linksDiv.style.fontSize = '10px';
                    linksDiv.style.display = 'none';
                    rowNode.cells[ 3 ].appendChild( linksDiv );
                    postsLink.addEventListener( 'click', genClick( linksDiv ), false ); // click handler to show/hide the linksDiv
                }
                // add the links to my posts
                linksDiv.innerHTML += 'My posts:';
                var pages = eval( GM_getValue( postsPrefix + thread_id ) );
                if ( ! pages ) {
                    // we set the state as "posted" before the post-tracking feature was added, so we don't have any pages to link to
                    linksDiv.innerHTML += '?';
                } else {
                    // got pages - add links
                    for ( var p = 0; p < pages.length; p++ ) {
                        if ( p > 0 ) linksDiv.innerHTML += ',';
                        linksDiv.innerHTML += ' <a href="forums_thread.php?thread_id=' + thread_id + '&page=' + pages[ p ] + '">' + pages[ p ] + '</a>';
                    }
                }
            }
            // create the links
            var fragment = document.createDocumentFragment();
            for ( var tl = 0; tl < threadLinks.length; tl++ ) {
                fragment.appendChild( threadLinks[ tl ] );
                if ( tl < threadLinks.length - 1 || div.firstChild ) {
                    // char 8226 is &bull;
                    // add bullets between links, and add an extra one if there's already links in the div (from another hack)
                    fragment.appendChild( document.createTextNode( ' ' + String.fromCharCode( '8226' ) + ' ' ) ); 
                }
            }
            // add the new links
            if ( div.firstChild ) {
                // already some links - insert the new ones in the right place
                div.insertBefore( fragment, div.firstChild.nextSibling );
            } else {
                // no extant links - add the label text and links
                div.appendChild( document.createTextNode( 'Jump to: ' ) );
                div.appendChild( fragment );
            }
            // move on
            l += lIncrement + threadLinks.length;
            while ( links[ l + 1 ].parentNode && links[ l + 1 ].parentNode.className.indexOf( 'linksDiv' ) > -1 ) {
                // skip over any links we might have added in linksDiv
                l++;
            }
        }
    }
}

// generates a closure for the onclick handler for showing/hiding the div with the page links
function genClick( linksDiv ) {
    return function ( event ) {
        if ( linksDiv.style.display == 'none' ) {
            linksDiv.style.display = '';
        } else {
            linksDiv.style.display = 'none';
        }
        event.preventDefault(); // don't actually follow-through on the click
    };
}

// manage the stack of page references for a thread
function insertPage( thread_id, page ) {
    // stored as a serialised array
    var posts = GM_getValue( postsPrefix + thread_id );
    posts = ( posts == null ) ? [] : eval( posts );

    // check to see if it's already in there
    var known = false;
    for ( var p = 0; p < posts.length; p++ ) {
        if ( posts[ p ] == page ) {
            known = true;
            break;
        }
    }
    if ( ! known ) {
        // it's not - add it and sort the contents in numeric order
        posts.push( page );
        posts = posts.sort( function ( a, b ) { return Number( a ) - Number( b ) } );
        // and store the value
        GM_setValue( postsPrefix + thread_id, uneval( posts ) );
    }
}

// find out who I'm logged in as
function whoami ( username ) {
    var currentSessid;
    var cookies = document.cookie.split( '; ' );

    // track session ID, to cover logging out by closing the browser
    for ( var c = 0; c < cookies.length; c++ ) {
        var bits = cookies[ c ].split( '=' );
        if ( bits[ 0 ] == "PHPSESSID" ) {
            currentSessid = bits[ 1 ];
            break;
        }
    }

    // I've been given a username - set it and be done
    if ( username != null ) {
        GM_setValue( 'username', username );
        GM_setValue( 'sessid', currentSessid );
        return;
    }

    var storedSessid = GM_getValue( 'sessid' );
    var username = GM_getValue( 'username' );
    if ( storedSessid == currentSessid && username!= "" && username != null ) {
        // we have a username for the current session - return it
        return username;
    }
    // find the user name by parsing links
    var links = document.getElementsByTagName( 'a' );
    for ( var l = 0; l < links.length; l++ ) {
        var link = links[ l ];
        if ( link.href ) {
            if ( link.href.indexOf( 'login.php' ) > -1 ) {
                // not logged in - no username
                return null;
            }
            if ( link.href.indexOf( 'your_etsy.php' ) > -1  ){
                // username is next to the Your Etsy link
                var cell = link.parentNode;
                var text = cell.parentNode.cells[ cell.cellIndex - 1 ].innerHTML;
                text = text.substring( 0, text.indexOf( ':' ) );
                // set the values and return
                GM_setValue( 'username', text );
                GM_setValue( 'sessid', currentSessid );
                return text;
            }
         }
     }
}

// add a listener to clear the stored username when you log out
function decorateLogout () {
    var links = document.getElementsByTagName( 'a' );
    for ( var l = 0; l < links.length; l++ ) {
        var link = links[ l ];
        if ( link.href ) {
            if ( link.href.indexOf( 'login.php' ) > -1 ) {
                // not logged in - bail out
                return;
            }
            if ( link.href.indexOf( 'logout.php' ) > -1 ) {
                // add listener to clear stored username
                link.addEventListener( "click", function () { whoami( "" ) }, true );
            }
        }
    }
}    

// utility function to replicate getElementsByClassName() on older Firefoxes
function getElementsByClassName ( class, node ) {
    if ( node == null ) node = document;
    if ( node.getElementsByClassName ) {
        return node.getElementsByClassName( class );
    } else {
        var classElements = new Array();
        var els = node.getElementsByTagName( '*' );
        var elsLen = els.length;
        var pattern = new RegExp("(^|\\s)"+class+"(\\s|$)");
        for (i = 0, j = 0; i < elsLen; i++) {
            if ( pattern.test(els[i].className) ) {
                classElements[j] = els[i];
                j++;
            }
        }
        return classElements;
    }
}


