///////////////////////////////////////////////////////////////////////////////
//
// YSlow Yawn Meter: A YSlow tool that shows the total time you've spent waiting for pages to load.
// Version 1.0, 2009-03-04
// Coded by Maarten van Egmond.
// Released under the GPL license: http://www.gnu.org/copyleft/gpl.html
//
///////////////////////////////////////////////////////////////////////////////

// Please JSLint.
/*global alert, YSLOW */

// Singleton pattern.
var YSlowYawnMeter = function () {
    //
    // Private variables
    //

    // CSS for output.
    var cssOutput;

    // To calculate a page load time, we need to store the last unload time.
    var lastUnloadTime = undefined;

    // The stats show a range between minimum load time (DOM only) and maximum
    // load time (DOM + external assets).
    var totalMinTime = 0;
    var totalMaxTime = 0;
    var totalPages = 0;
    // Keep track of load time for min portion (DOM only).
    var sawMinTime = false;
    var minTime = 0;

    // Page loads can be interrupted, and incomplete loads should not be added
    // to the main stats.  But for completeness, we'll report those seperately.
    // At a minimum, the DOM needs to have been loaded to receive an event, so
    // we'll call these total "DOM" load time.
    var totalDomTime = 0;
    var totalDomPages = 0;

    // For debugging only.
    var debugMode = false;
    var debugPageCount = 0;
    var debugLog = [];

    //
    // Private functions
    //

    function concatenate(args) {
        return args.join('');
    }

    function formatDateTime(ms) {
        var d = new Date();
        if (ms) {
            d.setTime(ms);
        }

        var mon = d.getMonth() + 1;
        var day = d.getDate();
        var hrs = d.getHours();
        var min = d.getMinutes();
        var sec = d.getSeconds(); 

        // Avoid performance hit by using so many + operators.
        return concatenate([
                d.getFullYear(),
                '/', (mon < 10 ? '0' : ''), mon,
                '/', (day < 10 ? '0' : ''), day,
                ' ', (hrs < 10 ? '0' : ''), hrs,
                ':', (min < 10 ? '0' : ''), min,
                ':', (sec < 10 ? '0' : ''), sec
            ]);
    }

    function formatTime(t, inclMs) {
        var s = Math.floor(t / 1000);
        var ms = t - (s * 1000);
        var m = Math.floor(s / 60);
        s = s - (m * 60);
        var h = Math.floor(m / 60);
        m = m - (h * 60);

        // Avoid performance hit by using so many + operators.
        var buf = [
                (h < 10 ? '0' : ''), h, 'h:',
                (m < 10 ? '0' : ''), m, 'm:',
                (s < 10 ? '0' : ''), s
            ];

        if (inclMs) {
            buf.push('.', (ms < 100 ? '0' : ''), (ms < 10 ? '0' : ''), ms);
        }

        buf.push('s');

        return concatenate(buf);
    }

    function debugAddLog(str) {
        debugLog[debugLog.length] = formatDateTime() + ': ' + str;
    }

    function onPageUnload(e) {
        // A page's total load time is calculated as:
        // (a page's onLoad time) - (the previous page's onUnload time)
        // so start by capturing the onUnload time.

        if (debugMode) {
            var d = new Date();
            d.setTime(e.time);
            debugAddLog('onPageUnload: url = ' + e.window.document.URL +
                    ', e.time = ' + formatDateTime(e.time));
        }

        lastUnloadTime = e.time;
    }

    function onDomReady(e) {
        // A page's DOM-only load time is calculated as:
        // (a page's onDomReady time) - (the previous page's onUnload time)
        // so if we have the last onUnload time, we can proceed.

        // TODO: would like to make sure that the unload event recorded earlier
        // and this event are for the same page...

        if (debugMode) {
            debugPageCount++;
            debugAddLog('onDomReady: url = ' + e.window.document.URL +
                    ', e.time = ' + formatDateTime(e.time));
        }

        sawMinTime = false;
        if (lastUnloadTime !== undefined && lastUnloadTime !== 0) {
            var loadTime = e.time - lastUnloadTime;
            if (loadTime > 0) {
                // Always add to DOM stats.
                totalDomTime += loadTime;
                totalDomPages++;

                // Save min time.
                sawMinTime = true;
                minTime = loadTime;

                if (debugMode) {
                    debugAddLog('onDomReady: url = ' + e.window.document.URL +
                            ', loadTime = ' + loadTime +
                            ', e.time = ' + formatDateTime(e.time));
                }
            } else {
                if (debugMode) {
                    alert('Error: got onDomReady event with bogus time? (' +
                            e.time + ' - ' + lastUnloadTime + ' = ' +
                            loadTime + ')');
                }
            }
        } else {
            if (debugMode) {
                // The very first page load would get here so can't alert here.
                //alert('Error: got onDomReady event without seeing onPageUnload event');
            }
        }
    }

    function onPageLoad(e) {
        // A page's total load time is calculated as:
        // (a page's onLoad time) - (the previous page's onUnload time)
        // so if we have the last onUnload time, we can proceed.

        // TODO: would like to make sure that the unload event recorded earlier
        // and this event are for the same page...

        if (debugMode) {
            debugAddLog('onPageLoad: url = ' + e.window.document.URL +
                    ', e.time = ' + formatDateTime(e.time));
        }

        if (lastUnloadTime !== undefined && lastUnloadTime !== 0) {
            var loadTime = e.time - lastUnloadTime;
            if (loadTime > 0) {
                if (sawMinTime) {
                    // Add to page stats.
                    totalMinTime += minTime;
                    totalMaxTime += loadTime;
                    totalPages++;
                } else {
                    if (debugMode) {
                        alert('Error: got onPageLoad event without DOM event?');
                    }
                }

                if (debugMode) {
                    debugAddLog('onPageLoad: url = ' + e.window.document.URL +
                            ', loadTime = ' + loadTime +
                            ', e.time = ' + formatDateTime(e.time));
                }
            } else {
                if (debugMode) {
                    alert('Error: got onPageLoad event with bogus time? (' +
                            e.time + ' - ' + lastUnloadTime + ' = ' +
                            loadTime + ')');
                }
            }
        } else {
            if (debugMode) {
                // The very first page load would get here so can't alert here.
                //alert('Error: got onPageLoad event without seeing onPageUnload event');
            }
        }
    }

    function getCssForOutput() {
        // Avoid performance hit by using so many + operators.
        var buf = [
                'div#toolOutput {\n',
                '    text-align: center;\n',
                '}\n',
                '\n',
                'p, h3, ul, ol {\n',
                '    text-align: left;\n',
                '}\n',
                '\n',
                'p#tagLine {\n',
                '    border-top: 1px solid black;\n',
                '    display: inline;\n',
                '    position: relative;\n',
                '    top: -1.25em;\n',
                '}\n',
                '\n',
                'p#highLevelResult {\n',
                '    border: 1px solid black;\n',
                '    font-size: 1.5em;\n',
                '    height: 11.5em;\n',
                '    margin: 1em auto 2em auto;\n',
                '    padding: 0 1em 1em 1em;\n',
                '    width: 28em;\n',
                '\n',
                '    /* IE7 only */\n',
                '    * height: 12em;\n',
                '    * width: 30em;\n',
                '}\n',
                '\n',
                'span#noData {\n',
                '    position: relative;\n',
                '    top: 5.5em;\n',
                '    left: 7.25em;\n',
                '}\n',
                '\n',
                'span#totalTime {\n',
                '    font-size: 2.5em;\n',
                '    padding: 0.25em;\n',
                '    position: relative;\n',
                '    top: 0.5em;\n',
                '}\n',
                '\n',
                'span#waiting {\n',
                '    display: block;\n',
                '    margin-left: 17.75em;\n',
                '    position: relative;\n',
                '    top: -0.125em;\n',
                '}\n',
                '\n',
                'span#toAppear {\n',
                '    display: block;\n',
                '    margin-left: 17.75em;\n',
                '}\n',
                '\n',
                'span#addToThis {\n',
                '    display: block;\n',
                '    position: relative;\n',
                '    top: 0.75em;\n',
                '}\n',
                '\n',
                'span#extraTime {\n',
                '    display: block;\n',
                '    font-weight: bold;\n',
                '    left: 2.125em;\n',
                '    position: relative;\n',
                '    top: 0.875em;\n',
                '}\n',
                '\n',
                'span#realize {\n',
                '    display: block;\n',
                '    left: 4.375em;\n',
                '    position: relative;\n',
                '    top: 1em;\n',
                '}\n',
                '\n',
                'span#life {\n',
                '    display: block;\n',
                '    left: 6.625em;\n',
                '    position: relative;\n',
                '    top: 1.125em;\n',
                '}\n'
            ];
        return concatenate(buf);
    }

    function runTool(doc, cset) {
        // Avoid performance hit by using so many + operators.
        var buf = [
                '<div id="toolOutput">\n',
                '\n',
                '<h1>YSlow Yawn Meter</h1>\n',
                '\n',
                '<p id="tagLine">Shows the total time you\'ve spent waiting for pages to load.</p>\n',
                '\n',
                '<p id="highLevelResult">'
            ];

        if (0 === totalDomPages) {
            buf.push('<span id="noData">No data yet.  Please reload a page.</span>');
        } else {
            buf.push(
                    'So far... <span id="totalTime">',
                    formatTime(totalDomTime, false),
                    '</span> <span id="waiting">was spent waiting</span> <span id="toAppear">just for pages to <em>appear</em>!</span> <span id="addToThis"><em>Add</em> to this</span> <span id="extraTime">the time it takes for images, etc. to load</span> <span id="realize">and you begin to realize...</span> <span id="life">how much of your life... is spent... just... waiting.</span>');
        }

        buf.push(
                '</p>\n',
                '\n',
                '<h3>Load Time Summary</h3>\n',
                '\n',
                '<p>');

        if (0 === totalDomPages) {
            buf.push('No data yet.  Please reload a page.');
        } else {
            buf.push(
                    'There were ',
                    totalDomPages,
                    ' pages for which a DOM event was received.<br />The total loading time for those pages was ',
                    formatTime(totalDomTime, true),
                    ', or ',
                    ((totalDomTime / totalDomPages).toFixed(0) / 1000).toFixed(3),
                    's per page.</p>\n',
                    '\n',
                    '<p>There were ',
                    totalPages,
                    ' pages for which both a DOM event and a page-complete event was received.<br />The total DOM loading time for those pages was ',
                    formatTime(totalMinTime, true),
                    ', or ',
                    ((totalMinTime / totalPages).toFixed(0) / 1000).toFixed(3),
                    's per page.<br />The total page-complete loading time for those pages was ',
                    formatTime(totalMaxTime, true),
                    ', or ',
                    ((totalMaxTime / totalPages).toFixed(0) / 1000).toFixed(3),
                    's per page.');
        }

        buf.push(
                '</p>\n',
                '\n');

        if (debugMode) {
            buf.push(
                    '</p>\n',
                    '\n',
                    '<h3>Debug Data</h3>\n',
                    '\n',
                    '<p>Counted ', debugPageCount, ' DOM events.</p>\n',
                    '<ol>\n');

            for (var ii = 0; ii < debugLog.length; ii++) {
                buf.push('<li>', debugLog[ii], '</li>\n');
            }

            buf.push(
                    '</ol>\n',
                    '\n');
        }

        buf.push(
                '<h3>Using the YSlow Yawn Meter</h3>\n',
                '\n',
                '<p>The YSlow Yawn Meter is more accurate if you follow the following suggestions.</p>\n',
                '\n',
                '<ul>\n',
                '<li>Enable the YSlow "Autorun" option via the YSlow icon in the task bar, or check the "Auto run test every time you launch YSlow" checkbox in the YSlow window (so load times are counted automatically)</li>\n',
                '<li>Use only one browser window (otherwise two separate instances of YSlow are running and load times in window 1 will not be counted in window 2)</li>\n',
                '<li>Use only one browser tab, if your browser supports tabbed browsing (opening a link in a new tab or window does not cause a page unload event so the load time for that pages is not counted)</li>\n',
                '<li>If you do use multiple tabs, use only one tab at a time (there is just one page unload event handler for all tabs, so (re)loading multiple pages at the same time may confuse the tool)</li>\n',
                '</ul>\n',
                '\n',
                '<h3>How the YSlow Yawn Meter Works</h3>\n',
                '\n',
                '<p>Three events are used to collect data about a page\'s load time.  The first occurs when the user navigates away from the current page.  This triggers an <em>unload</em> event, which is the start of the new page\'s load timer.  The second event occurs when just the DOM portion of the new page has loaded (the DOM is equivalent to what you see when you view the HTML source of a page).  The third event occurs when all external elements like CSS, JavaScript and images have finished loading and the new page is complete.</p>\n',
                '\n',
                '<p>The load time that YSlow reports is simply the amount of time that passed between the unload event of the previous page and the page-complete time of the new page.  To create a summation of all these load times, a tool must take into account that in regular web browsing, users don\'t always wait for pages to completely finish loading.  A user could press the browser\'s stop button or navigate to another page before the page-complete event occurs.  (It could also happen that a page is interrupted before even the DOM event occurs; since no DOM event is received, the tool does not add any wait time to account for those cases.)</p>\n',
                '\n',
                '<p>The time shown at the top of this page was determined by summing up the elapsed time between the unload and DOM events for all pages loaded since the browser started up (measured not per tab but across all tabs).  This is the <em>minimum</em> wait time as it does not include time for loading images, etc.  For pages that <em>were</em> able to finish loading completely, there is some additional data to look at, which is shown in the Load Time Summary.</p>\n',
                '\n',
                '</div>\n');
        var htmlOutput = concatenate(buf);

        return { 'html': htmlOutput, 'css': cssOutput };
    }

    // Return publicly accessible variables and functions.
    return {
        //
        // Public functions
        // (These access private variables and functions through "closure".)
        //

        // Initialize this script.
        init: function () {
            cssOutput = getCssForOutput();

            try {
                // Register the tool...
                YSLOW.registerTool({
                    id: 'YSlowYawnMeter',
                    name: 'Yawn Meter',
                    short_desc: 'Shows the total time you\'ve spent waiting for pages to load.',
                    print_output: true,
                    run: runTool
                });

                // ...and the events it needs to do its job.
                YSLOW.addEventListener("onload", onPageLoad, this);
                YSLOW.addEventListener("onUnload", onPageUnload, this);
                YSLOW.addEventListener("onDOMContentLoaded", onDomReady, this);
            } catch (err) {
                alert("YSlowYawnMeter.init: " + err);
            }

            // Now wait for events to happen.
        }
    };
}();
// End singleton pattern.

// Only initialize this Firefox add-on if YSlow is installed.
if (typeof YSLOW === "undefined") {
    alert("YSlow2 must be installed to use this plugin.\n" +
            "You can get it here: " +
            "https://addons.mozilla.org/en-US/firefox/addon/5369");
} else {
    YSlowYawnMeter.init();
}

///////////////////////////////////////////////////////////////////////////////


