// ==UserScript==
// @name           MetaFilter Scroll Tag
// @namespace      http://plutor.org/
// @description    Tracks your last-read comment in threads, and allows you to jump back to it easily.
// @include        http://metafilter.com/*
// @include        http://*.metafilter.com/*
// @include        file:///home/lingalls/*
// ==/UserScript==
//
// DONE
// * Dropping the tag above the first comment
// * Dropping/following the tag below the last comment (next/last div)
// + Fade in jumper?
// * What if there are zero comments?
// + Link to marker from list pages
// TODO
// * Hiding time for jumper seems to be cumulative. Why?
// ~ Comments

var thread_id;
var markedcomment;
var comments = new Array();
var lastscroll;
var nextshow;
var markerdat;

//
// mst_init
//
// Determines if this is a thread or not, and sets everything up
//
function mst_init() {
    thread_id = mst_threadUniqName(location.href);

    if (thread_id) {
        // This is a thread
        mst_initComments();
        mst_loadValue();
        setInterval( function() { mst_findTopComment() }, 200 );
        mst_showJumper();
        document.addEventListener('scroll', function() { mst_hideJumper() }, false);
    } else {
        // This is not a thread
        mst_findThreads();
    }
}

//
// mst_threadUniqName
//
// Argument: url = the url of the thread
//
// Returns a uniq name of the thread: "$subdomain $threadid"
//
function mst_threadUniqName(url) {
    var urlm = url.match(/^http:\/\/([^\/]*\.)?metafilter.com\/(\d+)\//);
    if (urlm)
        return urlm[1] + urlm[2];
    return "";
}

//
// mst_initComments
//
// Finds all of the comment DOM objects and puts them into an array
// for easy future use
//
function mst_initComments() {
    var alldivs = document.getElementsByTagName('div');
    for (var i=0; i<alldivs.length; ++i) {
        if (alldivs[i].className == 'comments' &&
            alldivs[i].id != 'prevDiv' && alldivs[i].id != 'prevDiv2' &&
            alldivs[i].innerHTML.match(/posted by/)) {
            comments.push(alldivs[i]);
        } // is a comment
    } // loop divs
}

//
// mst_findThreads
//
// Finds all of the threads listed on the page, and replaces the '(mm new)'
// listings with ones that are more accurate.
//
function mst_findThreads() {
    var alldivs = document.getElementsByTagName('span');
    for (var i=0; i<alldivs.length; ++i) {
        if (alldivs[i].className == 'smallcopy' &&
            alldivs[i].innerHTML.match(/posted by/)) {
            var metabits = alldivs[i].childNodes;
            // First pass - delete 'mm new' links
            for (var j=0; j<metabits.length; ++j) {
                var bit = metabits[j];
                var m;
                if (bit.tagName && bit.tagName.toLowerCase() == 'b' &&
                    bit.innerHTML.match(/\d+ new/)) {
                    bit.parentNode.removeChild(bit);
                }
            } // loop bits of info

            // Second pass - add new 'mm new' links
            for (var j=0; j<metabits.length; ++j) {
                var bit = metabits[j];
                var m;
                if ( bit.tagName && bit.tagName.toLowerCase() == 'a' &&
                     (m = bit.innerHTML.match(/^(\d+) comments?$/)) ) {
                    var total = m[1];
                    var tid = mst_threadUniqName(bit.href);
                    if (tid) {
                        var markedid = GM_getValue(tid);
                        var markednum = GM_getValue(tid + "num");
                        var newc = total - markednum;
                        if (!markedid || !markednum) {
                            newc = total;
                            markedid = '';
                        }
                        if (newc > 0) {
                            // There are new comments
                            var newnew = document.createElement('b');
                            newnew.style.color = 'white';
                            newnew.style.marginLeft = '0.4em';
                            newnew.innerHTML = '(<a href="'
                                             + bit.href
                                             + '#'
                                             + markedid
                                             + '" class="new">'
                                             + newc
                                             + ' new</a>)';
                            bit.parentNode.insertBefore( newnew,
                                                         bit.nextSibling );
                        }
                    }
                } // is a comments link
            } // loop bits of info
        } // is a thread
    } // loop divs

}

//
// mst_loadValue
//
// Argument: t = thread id (optional, defaults to global thread_id)
//
// Gets the comment id for the thread, finds the first comment after the
// anchor with that name, and marks it
//
function mst_loadValue(t) {
    if (!t) t = thread_id;
    var comment_id = GM_getValue(t);

    // Find the anchor
    var anchors = document.getElementsByTagName('a');
    for (var i=0; i<anchors.length; ++i) {
        if (anchors[i].name == comment_id) {
            // Find the comment div after the anchor
            var a = anchors[i];
            while (a && (!a.className || a.className != 'comments' &&
                         a.id != 'prevDiv' && a.id != 'prevDiv2')) {
                a = a.previousSibling;
            }
            
            if (a) {
                markedcomment = a;
                mst_mark(a);
            }

            break;
        }
    }
}

//
// mst_findTopComment
//
// Argument: ysc = number of pixels from the top (optional, defaults to current position)
// Argument: dropped = is this is a search caused by a dropped marker?
//
// If dropped is false, finds the first comment below the scroll point. If it's
// below the currently marked comment, it marks it.
// If dropped is true, finds the last comment above the scroll point and marks it.
//
function mst_findTopComment(ysc, dropped) {
    if (!ysc) ysc = window.scrollY;
    var topc;

    if (!dropped) {
        if (ysc == lastscroll) return;
        lastscroll = ysc;
    }

    // Assuming the comments array is in order
    for (var i=0; i<comments.length; ++i) {
        var c = comments[i];
        var cy = c.offsetTop;

        if (!topc)
            topc = c;

        if (dropped) {
            // Trying to find the last comment above ysc
            if ( cy <= ysc ) {
                topc = c;
            } else {
                break;
            }
        } else {
            // Trying to find the first comment below ysc
            topc = c;
            if ( cy >= ysc ) {
                break;
            }
        }
    }

    if (topc && (dropped || !markedcomment || markedcomment.offsetTop < topc.offsetTop)) {
        mst_mark(topc);
    }
}

//
// mst_mark
//
// Argument: c = DOM object of the comment to mark
//
// Moves the marker to the comment indicated, and saves the comment id
//
function mst_mark(c) {
    var marker = document.getElementById("mst_marker");
    if (!marker) {
        // Create it
        marker = document.createElement('div');
        marker.id = "mst_marker";
        marker.style.backgroundImage = 'url(' + marker_img + ')';
        marker.style.backgroundRepeat = 'no-repeat';
        marker.style.width = '40px';
        marker.style.height = '25px';
        marker.style.position = 'absolute';

        var grabber = document.createElement('div');
        grabber.id = "mst_marker_grabber";
        grabber.style.width = '10px';
        grabber.style.height = '25px';
        grabber.style.position = 'relative';
        grabber.style.top = "0px";
        grabber.style.left = "0px";
        grabber.style.cursor = "move";

        marker.appendChild(grabber);
        document.body.appendChild(marker);
        grabber.addEventListener('mousedown', mst_grabMarker, false);
    }

    // Mark - top aligned
    marker.style.top = "" + (c.offsetTop) + "px";
    marker.style.left = "15px";

    markedcomment = c;
    var comment_id = mst_commentIdOf(c);
    var comment_num = mst_commentNumOf(c);

    // Save
    if (thread_id && comment_id && comment_num) {
        GM_setValue(thread_id, comment_id);
        GM_setValue(thread_id + "num", comment_num);
    }
}

//
// mst_commentIdOf
//
// Argument: c = DOM object of the comment
//
// Returns the comment_id pulled from the name of the anchor immediately
// before the given comment
//
function mst_commentIdOf(c) {
    // Find the anchor right before the comment div
    while (c && (!c.tagName || c.tagName.toLowerCase() != 'a')) {
        c = c.previousSibling;
    }
    return (c) ? c.name : null;
}

//
// mst_commentIdOf
//
// Argument: c = DOM object of the comment
//
// Returns the index(+1) of the given comment in the comments array
//
function mst_commentNumOf(c) {
    for (var i=0; i<comments.length; ++i) {
        if (comments[i] == c) {
            return i+1;
        }
    }
    
    return 0;
}

//
// mst_showJumper
//
function mst_showJumper(fade) {
    if (!markedcomment) return;

    var winbottom = window.scrollY + window.innerHeight;
    var commenty = markedcomment.offsetTop;

    if (commenty > winbottom) {
        // Show it
        var jumper = document.getElementById("mst_jumper");
        if (!jumper) {
            // Create it
            jumper = document.createElement('div');
            jumper.id = "mst_jumper";
            jumper.style.position = 'absolute';
            jumper.style.backgroundImage = 'url(' + jumper_img + ')';
            jumper.style.backgroundRepeat = 'no-repeat';
            jumper.style.height = '40px';
            jumper.style.width = '25px';
            jumper.style.cursor = 'pointer';

            var grabber = document.createElement('div');
            grabber.id = "mst_jumper_grabber";
            grabber.style.width = '25px';
            grabber.style.height = '10px';
            grabber.style.position = 'relative';
            grabber.style.top = "0px";
            grabber.style.left = "0px";
            grabber.style.cursor = "move";

            jumper.appendChild(grabber);
            document.body.appendChild(jumper);

            grabber.addEventListener('mousedown', mst_grabMarker, false);
            jumper.addEventListener('click', mst_useJumper, false);
        }

        jumper.style.display = 'block';
        jumper.style.top = "" + (winbottom - 45) + "px";
        jumper.style.left = "22px";

        if (!fade)  { fade = 0; }
        fade += 0.10;
        jumper.style.MozOpacity = fade;
        if (fade < 1) {
            nextshow = setTimeout( mst_showJumper, 25, fade );
        }
    }
}

//
// mst_useJumper
//
function mst_useJumper() {
    // Go to the marked comment
    var id = mst_commentIdOf(markedcomment);
    if (id) location.href = "#" + id;
}

//
// mst_hideJumper
//
function mst_hideJumper(keephidden) {
    var jumper = document.getElementById("mst_jumper");

    if (jumper) {
        jumper.style.display = 'none';
    }

    if (nextshow) {
        clearTimeout(nextshow);
        nextshow = null;
    }

    if (!keephidden) nextshow = setTimeout( mst_showJumper, 100 );
}

//
// mst_grabMarker
//
function mst_grabMarker(e) {
    var grabber = document.getElementById('mst_marker_grabber');

    // Remove the mousedown event
    grabber.removeEventListener('mousedown', mst_grabMarker, false);

    // Add mousemove/mouseup events
    document.body.addEventListener('mousemove', mst_moveMarker, false);
    document.body.addEventListener('mouseup', mst_dropMarker, false);

    // Record where we started
    markerdat = {
                  x: e.pageX,
                  y: e.pageY,
                  g: grabber,
                  init: 0
                };

    e.preventDefault();
    return false;
}

//
// mst_moveMarker
//
function mst_moveMarker(e) {
    var x = e.pageX;
    var y = e.pageY;

    // Is this the initial movement
    if (!markerdat.init) {
        // Is initial movement sufficient?
        if ( Math.abs(markerdat.x-x) + Math.abs(markerdat.y-y) > 15 ) {
            // Hide the jumper, don't start redisplay timer
            markerdat.init = 1;
            mst_hideJumper(1);
        }
    }

    // Show the marker under the mouse
    if (markerdat.init) {
        var marker = document.getElementById('mst_marker');
        marker.style.MozOpacity = '0.7';
        marker.style.top = "" + (y - 12) + "px";
        marker.style.left = "" + (x - 5) + "px";
    }

    e.preventDefault();
    return false;
}

//
// mst_dropMarker
//
function mst_dropMarker(e) {
    // Did we ever have movement?
    if (markerdat.init) {
        var marker = document.getElementById('mst_marker');
        marker.style.MozOpacity = '1.0';

        // Record the new marked location
        mst_findTopComment(e.pageY, 1);
    }

    mst_showJumper();

    // Re-add the mousedown 
    markerdat.g.addEventListener('mousedown', mst_grabMarker, false);

    // Remove mousemove/mouseup events
    document.body.removeEventListener('mousemove', mst_moveMarker, false);
    document.body.removeEventListener('mouseup', mst_dropMarker, false);
}

var marker_img = "data:image/gif;base64,R0lGODdhBgAEAPcAAL+/v7+/vwD/AP//AAAA//8A/wD//////9vb27a2tpKSkm1tbUlJSSQkJNsAALYAAJIAAG0AAEkAACQAAADbAAC2AACSAABtAABJAAAkANvbALa2AJKSAG1tAElJACQkAAAA2wAAtgAAkgAAbQAASQAAJNsA27YAtpIAkm0AbUkASSQAJADb2wC2tgCSkgBtbQBJSQAkJP/b29u2traSkpJtbW1JSUkkJP+2ttuSkrZtbZJJSW0kJP+SktttbbZJSZIkJP9tbdtJSbYkJP9JSdskJP8kJNv/27bbtpK2km2SbUltSSRJJLb/tpLbkm22bUmSSSRtJJL/km3bbUm2SSSSJG3/bUnbSSS2JEn/SSTbJCT/JNvb/7a225KStm1tkklJbSQkSba2/5KS221ttklJkiQkbZKS/21t20lJtiQkkm1t/0lJ2yQktklJ/yQk2yQk////29vbtra2kpKSbW1tSUlJJP//ttvbkra2bZKSSW1tJP//ktvbbba2SZKSJP//bdvbSba2JP//SdvbJP//JP/b/9u227aStpJtkm1JbUkkSf+2/9uS27ZttpJJkm0kbf+S/9tt27ZJtpIkkv9t/9tJ27Yktv9J/9sk2/8k/9v//7bb25K2tm2SkkltbSRJSbb//5Lb2222tkmSkiRtbZL//23b20m2tiSSkm3//0nb2yS2tkn//yTb2yT////bttu2kraSbZJtSW1JJEkkAP+2ktuSbbZtSZJJJG0kAP+229uStrZtkpJJbW0kSUkAJP+SttttkrZJbZIkSW0AJNu2/7aS25Jttm1JkkkkbSQASbaS/5Jt221JtkkkkiQAbbbb/5K2222StkltkiRJbQAkSZK2/22S20lttiRJkgAkbbb/25Lbtm22kkmSbSRtSQBJJJL/tm3bkkm2bSSSSQBtJNv/trbbkpK2bW2SSUltJCRJALb/kpLbbW22SUmSJCRtAP+2ANuSALZtAJJJAP8AttsAkrYAbZIASQC2/wCS2wBttgBJkgAAAAAAACwAAAAABgAEAAAIHgABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgA7";
var jumper_img = "data:image/gif;base64,R0lGODdhBgAEAPcAAL+/v7+/vwD/AP//AAAA//8A/wD//////9vb27a2tpKSkm1tbUlJSSQkJNsAALYAAJIAAG0AAEkAACQAAADbAAC2AACSAABtAABJAAAkANvbALa2AJKSAG1tAElJACQkAAAA2wAAtgAAkgAAbQAASQAAJNsA27YAtpIAkm0AbUkASSQAJADb2wC2tgCSkgBtbQBJSQAkJP/b29u2traSkpJtbW1JSUkkJP+2ttuSkrZtbZJJSW0kJP+SktttbbZJSZIkJP9tbdtJSbYkJP9JSdskJP8kJNv/27bbtpK2km2SbUltSSRJJLb/tpLbkm22bUmSSSRtJJL/km3bbUm2SSSSJG3/bUnbSSS2JEn/SSTbJCT/JNvb/7a225KStm1tkklJbSQkSba2/5KS221ttklJkiQkbZKS/21t20lJtiQkkm1t/0lJ2yQktklJ/yQk2yQk////29vbtra2kpKSbW1tSUlJJP//ttvbkra2bZKSSW1tJP//ktvbbba2SZKSJP//bdvbSba2JP//SdvbJP//JP/b/9u227aStpJtkm1JbUkkSf+2/9uS27ZttpJJkm0kbf+S/9tt27ZJtpIkkv9t/9tJ27Yktv9J/9sk2/8k/9v//7bb25K2tm2SkkltbSRJSbb//5Lb2222tkmSkiRtbZL//23b20m2tiSSkm3//0nb2yS2tkn//yTb2yT////bttu2kraSbZJtSW1JJEkkAP+2ktuSbbZtSZJJJG0kAP+229uStrZtkpJJbW0kSUkAJP+SttttkrZJbZIkSW0AJNu2/7aS25Jttm1JkkkkbSQASbaS/5Jt221JtkkkkiQAbbbb/5K2222StkltkiRJbQAkSZK2/22S20lttiRJkgAkbbb/25Lbtm22kkmSbSRtSQBJJJL/tm3bkkm2bSSSSQBtJNv/trbbkpK2bW2SSUltJCRJALb/kpLbbW22SUmSJCRtAP+2ANuSALZtAJJJAP8AttsAkrYAbZIASQC2/wCS2wBttgBJkgAAAAAAACwAAAAABgAEAAAIHgABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgA7";

mst_init();

