// ==UserScript==
// @name                Sold orders plus
// @version             4.5
// @date                2010-06-17
// @author              Ian Malpass ( ian AT etsyhacks DOT com )
// @namespace           etsy.com
// @description         Adds the ability to search sold orders and see repeat buyers
// @include             http://www.etsy.com/your/listings/sold*
// @include             http://www.etsy.com/convo_view.php*
// @include             http://www.etsy.com/receipt.php*
// @include             http://www.etsy.com/your/orders/*
// ==/UserScript==

// star
var starUri = "data:image/gif,GIF89a%0C%00%0B%00%84%1D%00%FF%A0%40%FF%A3F%FF%A6M%FF%A7F%FF%A7O%FF%A8H%FF%A8Q%FF%ABM%FF%ADQ%FF%ADZ%FF%B0%60%FF%B2k%FF%B6i%FF%B8%5E%FF%B9%60%FF%B8t%FF%BAc%FF%BFg%FF%BE%81%FF%C1i%FF%C3v%FF%C4p%FF%C6%7D%FF%C9%83%FF%C9%8A%FF%CB%8E%FF%CE%93%FF%D3%9E%FF%DE%B6%FF%FF%FF%FF%FF%FF%FF%FF%FF!%F9%04%01%00%00%1F%00%2C%00%00%00%00%0C%00%0B%00%00%05-%E0'%8Ed)b%989b%D3%94%AAU%5B%95mmOc%DCF%08%20%D0%AD%01%00%60j%15%86D%DA%01%99%24%01%12%12I%A2%A9*%85%00%00%3B";

// zero-padded months, for convenience
var months = [ '01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12' ];

// things to import, when we're importing
var importStack = []; 

// flag to know if we're starting over
var importingAll = false;

// what data do we have already (populated later)
var importedFor;

// have we built the UI?
var ui_built = false;
var display_ui_built = false;

// should we automatically check for new data?
var autoUpdate = true;

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

if ( username != null ) {
    // add the option to build or rebuild the data store
    GM_registerMenuCommand( "Import all sales data", updateAll );

    // what data do we have?
    importedFor = eval( GM_getValue( username + '_importedDataFor' ) || '({})' );

    if ( GM_getValue( username + '_completedImport' ) ) {
        // data store has been set up
        // have to option to clear it
        GM_registerMenuCommand( "Clear sales data", clearData );

        // check auto-update settings
        autoUpdate = GM_getValue( username + '_autoUpdate' );
        if ( autoUpdate == null ) autoUpdate = true;

        // add menu items
        GM_registerMenuCommand( "Turn sales auto-update " + ( ( autoUpdate ) ? "off" : "on" ), function () { autoUpdate = ! autoUpdate; GM_setValue( username + '_autoUpdate', autoUpdate ) } );
        GM_registerMenuCommand( "Import latest sales data", updateData );

        // should we get the latest data?
        var now = new Date;
        var lastUpdate = GM_getValue( username + '_lastUpdate' ) || "0";
        lastUpdate = Number( lastUpdate );
        var do_auto_update = false;
        if ( document.location.href.indexOf( 'your/listings/sold' ) > -1 ) {
            var t = getFirstTransaction();
            if ( t && GM_getValue( 'transaction_' + t ) == null ) {
                do_auto_update = true;
            } else {
                GM_setValue( username + '_lastUpdate', String( now.valueOf() ) );
            }
        } else if ( document.location.href.indexOf( 'receipt.php' ) > -1 ) {
            var order_id = document.location.search.match( /order_id=(\d+)/ )[ 1 ];
            if ( GM_getValue( 'order_' + order_id ) == null ) {
                do_auto_update = true;
            }
        } else if ( document.location.href.indexOf( 'your/orders/' ) > -1 ) {
            var order_id = document.location.href..match( /your\/orders\/(\d+)/ )[ 1 ];
            if ( GM_getValue( 'order_' + order_id ) == null ) {
                do_auto_update = true;
            }
        } else if ( lastUpdate == null || ( now.valueOf() - lastUpdate ) > 7200000 ) {
            do_auto_update = true;
        }
        if ( autoUpdate && do_auto_update ) {
            // get the latest data; will build display UI after we have up-to-date data
            updateData();
        } else {
            // have up-to-date data - build the display UI
            buildDisplayUI();
        }
        // sort out the search user interface - don't need to wait for update
        buildUI();
    }
}

// find the first transaction on the sold orders page, so we can see if we need to update our data
function getFirstTransaction () {
    var links = document.getElementsByTagName( 'a' );
    var extractTransactionId = /\/transaction\/(\d+)/;
    for ( var l = 0; l < links.length; l++ ) {
        var link = links[ l ];
        if ( link.href ) {
            if ( link.href.indexOf( '/transaction/' ) > -1 ) {
                return link.href.match( extractTransactionId )[ 1 ];
            }
        }
    }
}

// function to sort out the user interface for the 
function buildUI () {
    // configure some styles to play nicely with other "order info" hacks
    var style = document.createElement( 'style' );
    style.type = 'text/css';
    style.innerHTML = '.etsyhacks_listing_id { display: none } .etsyhacks_order_notes { display: none } .etsyhacks_ship_to { display: none } .etsyhacks_current_order { background: #dddddd !important }';
    document.getElementsByTagName( 'head' )[ 0 ].appendChild( style );

    if ( document.location.href.indexOf( 'your/listings/sold' ) > -1 ) {
        // create the "search" box
        var gt = getElementsByClassName( 'month-filter' );
        /*
        var table = gt[ 0 ].parentNode.parentNode;
        var row = table.insertRow( 7 );
        table.insertBefore( table.rows[ 6 ].cloneNode( true ), row.nextSibling );
        var cell = row.insertCell( 0 );
        */
        var searchDiv = document.createElement( 'div' );
        gt[ 0 ].parentNode.insertBefore( searchDiv, gt[ 0 ] );
        searchDiv.innerHTML = '<form id="searchSalesForm"><nobr>Search orders: <input name="search" /> <input type="radio" name="stype" value="title" checked="checked" /> title <input type="radio" name="stype" value="buyer" /> buyer account <input type="radio" name="stype" value="buyer_name" /> shipping name&nbsp;&nbsp;<input type="button" value="Search" id="searchSold" /></nobr></form>';
        searchDiv.style.padding = '5px';
        var submit = document.getElementById( 'searchSold' );

        // add a click handler to the submit button to catch the submit - not going to do a server round trip
        submit.addEventListener( 'click', searchSold, false );

        // add a submit handler to the form to catch hitting "enter"
        searchDiv.getElementsByTagName( 'form' )[ 0 ].addEventListener( 'submit', searchSold, false );
    }
    // flag that we've rendered the UI, in case we do a re-import
    ui_built = true;
}

function buildDisplayUI () {
    if ( document.location.href.indexOf( 'your/listings/sold' ) > -1 ) {
        var query = document.getElementById( 'etsyhacks_sop_query' );
        if ( query == null ) {
            query = document.createElement( 'div' );
            query.id = 'etsyhacks_sop_query';
            query.style.display = 'none';
            document.body.appendChild( query );
        }
        query.innerHTML = 'ready';
        if ( GM_getValue( 'allowSOPQuery' ) != null ) query.addEventListener( 'change', parseQuery, false );
        var response = document.getElementById( 'etsyhacks_sop_response' );
        if ( response == null ) {
            response = document.createElement( 'div' );
            response.id = 'etsyhacks_sop_response';
            response.style.display = 'none';
            document.body.appendChild( response );
        }
        var evt = document.createEvent( 'MutationEvents' );
        evt.initMutationEvent( 'change', false, false, null, null, null, null, null );
        query.dispatchEvent( evt );
        var addr_fields = [ 'ship_to_name', 'addr_1', 'addr_2', 'city', 'state', 'code', 'country' ];
        // sort out "ship to" and listing IDs for other hacks
        var links = document.getElementsByTagName( 'a' );
        var extractTransactionId = /\/transaction\/(\d+)/;
        for ( var l = 0; l < links.length; l++ ) {
            var link = links[ l ];
            if ( link.href ) {
                if ( link.href.indexOf( '/transaction/' ) > -1 && link.innerHTML.indexOf( '<img' ) == -1 ) {
                    var transaction_id = link.href.match( extractTransactionId )[ 1 ];
                    var transaction = eval( GM_getValue( 'transaction_' + transaction_id ) || '({})' );
                    // found a transaction, and we have data for it
                    if ( transaction.listing ) {
                        // add a div with the listing ID in it
                        // it'll be hidden by default by the CSS we defined earlier
                        var div = document.createElement( 'div' );
                        div.style.fontSize = '11px';
                        div.className = 'etsyhacks_listing_id dark_grey_text';
                        div.innerHTML = 'Listing ID# ' + transaction.listing;
                        // attach it after the transaction link
                        link.parentNode.appendChild( div );
                    }
                } else if ( link.href.indexOf( '/receipt.php' ) > -1 || link.href.indexOf( '/your/orders/' ) > -1 ) {
                    // found the receipt link - find the order ID
                    var order_id = ( link.href.indexOf( 'receipt.php' ) > -1 ) ? link.href.match( /order_id=(\d+)/ )[ 1 ] : link.href.match( /\/your\/orders\/(\d+)/ )[ 1 ];
                    var order = eval( GM_getValue( 'order_' + order_id ) || '({})' );
                    var div = document.createElement( 'div' );
                    div.style.marginBottom = '5px';
                    div.className = 'etsyhacks_ship_to';
                    // look through the order's transactions and grab the first "ship to" name we find
                    for ( var t in order ) {
                        var transaction = eval( GM_getValue( 'transaction_' + t ) || '({})' );
                        if ( transaction.ship_to_name ) {
                            var addr = [];
                            for ( var a = 0; a < addr_fields.length; a++ ) {
                                if ( transaction[ addr_fields[ a ] ] ) {
                                    addr.push( transaction[ addr_fields[ a ] ] );
                                }
                            }
                            div.innerHTML = '<strong>Ship to:</strong> ' + addr.join( ', ' );
                            // got it, bail out
                            break;
                        } else {
                            div.innerHTML = '<strong>Ship to:</strong> <em>pending next sales data update</em>';
                        }

                    }
                    if ( div.innerHTML != '' ) {
                        // found something to show
                        // see if the order notes node already exists (created by another hack)
                        var cell = document.getElementById( 'order_notes_' + order_id );
                        if ( cell == null ) {
                            // doesn't already exist - build it
                            var node = link.parentNode;
                            while ( node.className.indexOf( 'odd' ) == -1 && node.className.indexOf( 'even' ) == -1 ) node = node.parentNode;
                            while ( ! node.className || node.className.indexOf( 'order-footer' ) == -1 ) node = node.nextSibling;
                            //if ( node.className.indexOf( 'receipt-group' ) > -1 ) {
                            //    nodeIdx = node.rowIndex + 1;
                            //    while ( nodeIdx < node.parentNode.rows.length && node.parentNode.rows[ nodeIdx ].cells.length < 6 ) nodeIdx++;
                            //    node = node.parentNode.rows[ nodeIdx - 1 ];
                            //}
                            var row = node.parentNode.insertRow( node.rowIndex - 1 );
                            // order notes will be hidden by default by the CSS we defined earlier
                            // another hack may reveal it
                            row.className = 'etsyhacks_order_notes';
                            cell = row.insertCell( 0 );
                            cell.colSpan = 6;
                            cell.style.padding = '5px 0px 0px 10px';
                            cell.style.fontSize = '11px';
                            cell.style.textAlign = 'left';
                            cell.className = 'dark_grey_text';
                            cell.id = 'order_notes_' + order_id;
                        }
                        // and add the "ship to" data to the order notes node
                        cell.appendChild( div );
                    }
                } else if ( link.href.indexOf( '/people/' ) > -1 ) {
                    var buyer = link.innerHTML.replace( /<\/?strong>/g, '' );
                    var orders = eval( GM_getValue( username + '_orders_' + buyer ) || '({})' );
                    var count = 0;
                    for ( var o in orders ) count++;
                    if ( count > 1 ) {
                        var img = document.createElement( 'img' );
                        img.src = starUri;
                        img.style.cursor = 'pointer';
                        img.style.display = 'inline';
                        img.style.border = 'none';
                        link.parentNode.appendChild( img, link.nextSibling );
                        img.addEventListener( 'click', genDisplayHistory( link.parentNode, buyer ), false );
                    }
                }
            }
        }
        GM_registerMenuCommand( 'Display sales stats', displayStats );
    } else if ( document.location.href.indexOf( 'convo_view.php' ) > -1 ) {
        // search for the "new convo" link to find username we're convoing with
        var links = document.getElementsByTagName( 'a' );
        for ( var l = 0; l < links.length; l++ ) {
            var link = links[ l ];
            if ( link.href && link.href.indexOf( '/convo_new.php?to_username' ) > -1 ) {
                buyer = link.href.match( /to_username=(.+)/ )[ 1 ];
                var ret = buyerTable( buyer );
                if ( ret[ 1 ] ) {
                    // we have sales data
                    var rdg = getElementsByClassName( 'row_dark_grey' );
                    var target = rdg[ rdg.length - 1 ].parentNode.parentNode;
                    // last row_dark_grey is the tags box - add the sales info after it
                    var r = target.parentNode.insertRow( 1 );
                    var cell = r.insertCell( 0 );
                    cell.style.height = '10px';
                    r = target.parentNode.insertRow( 2 );
                    cell = r.insertCell( 0 );
                    // add the sales data in a hidden container div
                    cell.appendChild( createContainer( ret[ 0 ], '500px', '0px', '0px' ) );
                    cell.className = 'left_align';
                    var holder = document.createElement( 'div' );
                    // add the sales summary
                    var html = '<table class="odd left_align" style="width: 200px;">';
                    html += '<tr><td style="padding: 10px;"><span class="dark_grey_text">Sales:</span> <a style="cursor: pointer">' + ret[ 1 ] + ' order' + ( ( ret[ 1 ] == 1 ) ? '' : 's' ) + '; ' + ret[ 2 ] + ' item' + ( ( ret[ 2 ] == 1 ) ? '' : 's' ) + '</a></td></tr>';
                    html += '</table>';
                    holder.innerHTML += html;
                    // add the show/hide click handler
                    holder.getElementsByTagName( 'a' )[ 0 ].addEventListener( 'click', genToggleTable(), false );
                    cell.appendChild( holder );
                }
                break;
            }
        }
    } else if ( document.location.href.indexOf( 'receipt.php' ) > -1 || document.location.href.indexOf( 'your/orders/' ) > -1 ) {
        var links = document.getElementsByTagName( 'a' );
        var order_id = ( link.href.indexOf( 'receipt.php' ) > -1 ) ? link.href.match( /order_id=(\d+)/ )[ 1 ] : link.href.match( /\/your\/orders\/(\d+)/ )[ 1 ];
        var buyer;
        var link;
        // find the user name
        for ( var l = 0; l < links.length; l++ ) {
            link = links[ l ];
            if ( link.href && link.href.indexOf( '/people/' ) > -1 ) {
                if ( link.innerHTML != username ) {
                    // found the buyer's profile link
                    buyer = link.innerHTML;
                    var ret = buyerTable( buyer, order_id );
                    if ( ret[ 1 ] > 1 ) {
                        // we have some other orders - we should show a link for the order history
                        var row = link.parentNode;
                        while ( row.nodeName != 'TABLE' ) row = row.parentNode;
                        row = row.parentNode;
                        while ( row.nodeName != 'TABLE' ) row = row.parentNode;
                        row = row.parentNode.parentNode;

                        // add an "orders" link next to the buyer's name
                        var span = document.createElement( 'span' );
                        span.innerHTML = ' (<a style="cursor: pointer">orders</a>)';
                        span.getElementsByTagName( 'a' )[ 0 ].addEventListener( 'click', genToggleTable(), false );
                        link.parentNode.insertBefore( span, link.nextSibling );

                        // add a cell to anchor the sales data container
                        var tCell = row.insertCell( 2 );
                        // add the sales data container to the cell
                        tCell.appendChild( createContainer( ret[ 0 ], '558px', '31px', '-2px' ) );
                    }            
                    break;
                }
            }
        }

        // seek transaction IDs to annotate with listing IDs for the "show listing IDs" hack
        var cells = document.getElementsByTagName( 'td' );
        for ( var c = 0; c < cells.length; c++ ) {
            var cell = cells[ c ];
            if ( cell.innerHTML.indexOf( 'Transaction ID#' ) == 0 ) {
                var transaction_id = cell.innerHTML.match( /\d+/ );
                var transaction = eval( GM_getValue( 'transaction_' + transaction_id ) || '({})' );
                if ( transaction.listing ) {
                    // found a transaction ID and we have a transaction record - add the listing ID
                    // span1 is the old transaction ID - the span means we can hide it with CSS easily
                    var span1 = document.createElement( 'span' );
                    span1.appendChild( cell.firstChild );
                    span1.className = 'etsyhacks_old_transaction_id';
                    cell.appendChild( span1 );
                    // span2 is the transaction ID plus listing ID, hidden by default
                    var span2 = document.createElement( 'span' );
                    span2.className = 'etsyhacks_listing_id';
                    span2.innerHTML = span1.innerHTML + '; Listing ID# ' + transaction.listing;
                    span2.style.fontSize = '11px';
                    cell.appendChild( span2 );
                }
            }
        }
    } else {
    }
    display_ui_built = true;
}        

function parseQuery () {
    var query = this.innerHTML;
    if ( query == 'ready' || query == '' ) return;
    try {
        query = eval( query );
        if ( query == null ) return;
        var searchField = query.searchField;
        var compare = query.compare;
        compare = compare.replace( '&lt;', '<' );
        compare = compare.replace( '&gt;', '>' );
        var transactions = eval( GM_getValue( username + '_transactions' ) || '({})' );
        var results = [];
        for ( var t in transactions ) {
            var transaction = eval( GM_getValue( 'transaction_' + t ) || '({})' );
            if (   ( compare == '==' && transaction[ searchField ] == query.searchFor )
                || ( compare == '>'  && transaction[ searchField ] >  query.searchFor )
                || ( compare == '<'  && transaction[ searchField ] <  query.searchFor )
                || ( compare == '>=' && transaction[ searchField ] >= query.searchFor )
                || ( compare == '<=' && transaction[ searchField ] <= query.searchFor )
            ) {
                var data = {}
                for ( var d in query.fields ) {
                    data[ query.fields[ d ] ] = transaction[ query.fields[ d ] ];
                }
                results.push( data );
            }
        }
        var resp = document.getElementById( 'etsyhacks_sop_response' );
        var ret = [];
        for ( var r = 0; r < results.length; r++ ) {
            var res = results[ r ];
            var bits = [];
            for ( var f in res ) {
                bits.push( '"' + f + '":"' + res[ f ] + '"' );
            }
            ret.push( '{' + bits.join( ',' ) + '}' );
        }
        resp.innerHTML = '[' + ret.join( ',' ) + ']';
        var evt = document.createEvent( 'MutationEvents' );
        evt.initMutationEvent( 'change', false, false, null, null, null, null, null );
        resp.dispatchEvent( evt );
    } catch ( e ) {
        alert( 'Error parsing query: ' + e );
    }
}

function genDisplayHistory( node, buyer ) {
    return function ( event ) {
        var ret = buyerTable( buyer );
        if ( ret[ 0 ] ) {
            var container = createContainer( ret[ 0 ], '552px', '0px', '-117px', '', 1000, true );
            node.appendChild( container );
        }
    }
}

// "close" button
function createClose () {
    var close_x = document.createElement( 'div' );
    close_x.innerHTML = 'close';
    close_x.style.cursor = 'pointer';
    close_x.style.color = '#999999';
    close_x.style.padding = '2px';
    close_x.style.position = 'absolute';
    close_x.style.top = '-1px';
    close_x.style.right = '-1px';
    close_x.style.border = '1px solid #dddddd';
    return close_x;
}

// search results container
function createContainer( html, width, top, right, display, zindex, clobber ) {
    var est = document.getElementById( 'etsyhacks_sales_table' );
    if ( est ) {
        est.parentNode.parentNode.removeChild( est.parentNode );
    }
    if ( display == null ) display = 'none';
    var container = document.createElement( 'div' );
    container.style.position = 'relative';
    var sales = document.createElement( 'div' );
    sales.style.padding = '5px';
    sales.style.border = '1px solid #dddddd';
    container.appendChild( sales );
    sales.innerHTML = '<style type="text/css">\n.searchRes { border-collapse: collapse } .searchRes tr.borderTop td { border-top: 1px solid #dddddd } .searchRes td, .searchRes th { padding: 5px }\n</style>';
    sales.style.background = '#ffffff';
    sales.style.display = display;
    sales.id = 'etsyhacks_sales_table';
    sales.style.position = 'absolute';
    sales.style.width = width;
    sales.style.top = top;
    sales.style.right = right;
    if ( zindex != null ) {
        sales.style.zIndex = zindex;
    }
    sales.innerHTML += html;
    var close = createClose();
    close.addEventListener( 'click', genToggleTable( null, clobber ), false );
    sales.appendChild( close );
    return container;
}

function displayStats () {
    var form = document.getElementById( 'searchSalesForm' );
    var div = buildSearchResDiv();
    form.appendChild( div );
    var table = buildStats();
    div.appendChild( table );
    // add a close button
    var close = createClose();
    close.addEventListener( 'click', function () { div.parentNode.removeChild( div ) }, false );
    div.appendChild( close );

}

// perform a search
function searchSold ( event ) {
    var form = ( event.target.nodeName == 'FORM' ) ? event.target : event.target.form;
    // don't actually want to submit the form
    event.preventDefault();

    // get the search parameters
    var search, type;
    for ( var e = 0; e < form.elements.length; e++ ) {
        var element = form.elements[ e ];
        if ( element.name == 'search' ) {
            search = element.value;
        } else if ( element.name == 'stype' ) {
            if ( element.checked ) type = element.value;
        }
    }

    var div = buildSearchResDiv();
    // attach it to the page
    form.appendChild( div );

    // perform the relevant search
    // subordinate searches will do the actual filling-in of the holder
    if ( type == 'buyer' ) {
        searchBuyer( search, div );
    } else if ( type == 'buyer_name' ) {
        searchTransactions( 'ship_to_name', search, div );
    } else if ( type == 'title' ) {
        searchTransactions( 'item', search, div );
    }

    // add a close button
    var close = createClose();
    close.addEventListener( 'click', function () { div.parentNode.removeChild( div ) }, false );
    div.appendChild( close );
}

function buildSearchResDiv () {
    // get the holder for the results    
    var div = document.getElementById( 'search_results_div' );
    if ( div == null ) {
        // no holder - build a new one
        div = document.createElement( 'div' );
        div.id = 'search_results_div';
        div.style.width = '553px';
        div.style.border = '1px solid #dddddd';
        div.style.background = '#ffffff';
        div.style.position = 'absolute';
        div.style.marginTop = '5px';
        div.style.marginLeft = '-5px';
        div.style.padding = '5px';
        div.style.zIndex = '1000';
    }
    div.innerHTML = '<style type="text/css">\n.searchRes { border-collapse: collapse } .searchRes tr.borderTop td { border-top: 1px solid #dddddd } .searchRes td, .searchRes th { padding: 5px }\n</style>';
    return div;
}

function searchBuyer( search, div ) {
    // case-insensitive partial match search
    var pattern = new RegExp( search, "i" );

    // pre-built cache of buyer names
    var buyers = eval( GM_getValue( username + '_buyers' ) || '({})' );

    // search the buyers object's keys
    var results = [];
    for ( var b in buyers ) {
        if ( b.match( pattern ) ) results.push( b );
    }
    results = results.sort( function ( a, b ) { return a.toLowerCase().localeCompare( b.toLowerCase() ) } );
    // display the results
    var html = "<p>Found " + results.length + " buyer" + ( ( results.length == 1 ) ? "" : "s" ) + " that matched \"" + search + "\"";
    html += ( results.length == 0 ) ? "." : ":";
    html += '</p>';
    div.innerHTML += html;
    for ( var r = 0; r < results.length; r++ ) {
        var ret = buyerTable( results[ r ] ); // build a table of the buyer's orders
        var resdiv = document.createElement( 'div' );
        var result = document.createElement( 'a' );
        result.style.cursor = 'pointer';
        result.innerHTML = results[ r ];
        resdiv.appendChild( result );
        resdiv.appendChild( document.createTextNode( ' (' + ret[ 1 ] + ' order' + ( ( ret[ 1 ] == 1 ) ? '' : 's' ) + '; ' + ret[ 2 ] + ' item' + ( ( ret[ 2 ] == 1 ) ? '' : 's' ) + ')' ) );
        var table = document.createElement( 'div' );

        // only display the results if there's only a single hit
        if ( results.length > 1 ) table.style.display = 'none';
        table.style.border = '1px solid #dddddd';
        table.style.marginBottom = '5px';
        table.innerHTML = ret[ 0 ];
        div.appendChild( resdiv );
        div.appendChild( table );

        // add a click handler to show/hide results for each buyer
        result.addEventListener( 'click', genToggleTable( table ), false );
    }
}

// function generator to create click handlers for showing/hiding tables of results
function genToggleTable ( node, remove ) {
    if ( node ) {
        return function ( event ) {
            if ( remove && node.style.display == '' ) {
                node.parentNode.removeChild( node );
            } else {
                node.style.display = ( node.style.display == 'none' ) ? '' : 'none';
            }
        }
    } else {
        return function ( event ) {
            var table = document.getElementById( 'etsyhacks_sales_table' );
            if ( table == null ) return;
            if ( remove && table.style.display == '' ) {
                table.parentNode.removeChild( table );
            } else {
                table.style.display = ( table.style.display == 'none' ) ? '' : 'none';
            }
        }
    }
}

function makeSearchRegExp ( search ) {
    var bits = search.split( /\s+/ );
    var search = [];
    for ( var b = 0; b < bits.length; b++ ) {
        var match = bits[ b ].match( /^('|")/ );
        if ( match ) {
            q = match[ 1 ];
            while ( bits[ b + 1 ] && bits[ b ].lastIndexOf( q ) != bits[ b ].length - 1 ) {
                bits[ b ] += " " + bits.splice( b + 1, 1 );
            }
            bits[ b ] = bits[ b ].substr( 1, bits[ b ].length - 2 );
        }
        search.push( new RegExp( bits[ b ], "i" ) );
    }
    return search;
}

function searchTransactions( field, search, div ) {
    // case-insensitive partial match search
    var pattern = makeSearchRegExp( search );
    
    // get a list of all transactions
    var transactions = eval( GM_getValue( username + '_transactions' ) || '({})' );

    // loop, opening each transaction and checking the requested field
    var results = [];
    for ( var t in transactions ) {
        var transaction = eval( GM_getValue( 'transaction_' + t ) || '({})' );
        if ( transaction[ field ] != null ) {
            var match = true;
            for ( var p = 0; p < pattern.length; p++ ) {
                match = match && transaction[ field ].match( pattern[ p ] );
            }
            if ( match ) results.push( t );
        }
    }

    // display the results
    var html = "<p>Found " + results.length + " item" + ( ( results.length == 1 ) ? "" : "s" ) + " that matched \"" + search + "\"";
    html += ( results.length == 0 ) ? "." : ":";
    html += '</p>';
    div.innerHTML += html;
    // reverse transaction id order is reverse chronological order
    results = results.sort( function ( a, b ) { return ( b - a ) } );
    var html = '<table class="searchRes"><tr><th>Buyer</th><th>Item</th><th>Qty</th><th>Price</th><th>Shipping</th><th>Date</th></tr>';
    for ( var t = 0; t < results.length; t++ ) {
        var transaction = eval( GM_getValue( 'transaction_' + results[ t ] ) || '({})' );
        var class = ( t % 2 ) ? '' : ' class="odd"';
        html += '<tr' + class + '><td>' + transaction.buyer + '</td><td><a href="http://www.etsy.com/transaction/' + results[ t ] + '">' + transaction.item + '</a></td><td>' + transaction.quantity + '</td><td align="right">$' + transaction.price + '</td><td align="right">$' + transaction.shipping + '</td><td><a href="http://www.etsy.com/receipt.php?order_id=' + transaction.order + '">' + transaction.date + '</a></td></tr>';
    }
    html += '</table>';
    div.innerHTML += html;    
}

// a table of orders from a specific buyer account
function buyerTable( buyer, order_id ) {
    var html = '<table id="' + buyer + '_sales" class="searchRes"><tr><th>Item</th><th>Qty</th><th>Price</th><th>Shipping</th><th>Date</th></tr>';
    var data = eval( GM_getValue( username + '_orders_' + buyer ) || '({})' );
    var oids = [];
    for ( var o in data ) oids.push( o );
    oids = oids.sort( function ( a, b ) { return ( b - a ) } );
    var tCount = 0;
    for ( var o = 0; o < oids.length; o++ ) {
        var transactions = eval( GM_getValue( 'order_' + oids[ o ] ) || '({})' );
        var tids = [];
        for ( var t in transactions ) tids.push( t );
        tids = tids.sort( function ( a, b ) { return ( b - a ) } ); // reverse chronological order
        for ( var t = 0; t < tids.length; t++ ) {
            var transaction = eval( GM_getValue( 'transaction_' + tids[ t ] ) || '({})' );
            tCount += Number( transaction.quantity );
            var classes = [];
            if ( ! ( o % 2 ) ) classes.push( 'odd' );
            if ( order_id != null && oids[ o ] == order_id ) classes.push( 'etsyhacks_current_order' );
            html += '<tr class="' + classes.join( " " ) + '" style="font-weight: normal">';
            html += '<td style="text-align: left"><a href="http://www.etsy.com/transaction/' + tids[ t ] + '">' + transaction.item + '</a></td><td style="text-align: left">' + transaction.quantity + '</td><td style="text-align: right">$' + transaction.price + '</td><td style="text-align: right; border-right: 1px solid #EEEEEE">$' + transaction.shipping + '</td>';
            if ( t == 0 ) {
                html += '<td style="vertical-align: middle" rowspan="' + tids.length + '"><a href="http://www.etsy.com/receipt.php?order_id=' + transaction.order + '">' + transaction.date + '</a></td>';
            }
            html += '</tr>';
        }
    }
    html += '</table>';
    return [ html, oids.length, tCount ]; // table, number of orders, number of items
}

// clear the decks before a re-import
function clearData () {
    importedFor = {}; // clear the current in-memory cache
    GM_setValue( username + '_importedDataFor', false ); // and the on-disk one
    GM_setValue( username + '_lastUpdate', '0' ); // never updated
    GM_setValue( username + '_completedImport', false ); // haven't imported

    var buyers = eval( GM_getValue( username + '_buyers' ) || '({})' );
    var orders = eval( GM_getValue( username + '_orders' ) || '({})' );
    var transactions = eval( GM_getValue( username + '_transactions' ) || '({})' );

    // clear the transactions
    for ( var t in transactions ) {
        GM_setValue( 'transaction_' + t, false );
    }

    // clear the orders
    for ( var o in orders ) {
        GM_setValue( 'order_' + o, false );
    }

    // clear the buyer caches
    for ( var b in buyers ) {
        GM_setValue( username + '_transactions_' + b, false );
        GM_setValue( username + '_orders_' + b, false );
    }

    // and clear all the indexes
    GM_setValue( username + '_buyers', false );
    GM_setValue( username + '_orders', false );
    GM_setValue( username + '_transactions', false );
}

// get the latest data
function updateData () {
    var now = new Date;
    var year = now.getFullYear();
    var m = now.getMonth();
    var url = 'http://www.etsy.com/your/listings/sold.csv?month=' + months[ m ] + '&year=' + year;
    importStack.push( url ); // current month

    // loop to make sure we get all the data from previous months
    // if we've never imported this month's data, we need last month's to make sure we have everything
    // make it a loop to allow for a multi-month hiatus
    while ( ! importedFor[ url ] ) {
        m--;
        if ( m < 0 ) {
            m = 11;
            year--;
            if ( year == 2004 ) return; // shouldn't ever happen....
        }
        url = 'http://www.etsy.com/your/listings/sold.csv?month=' + months[ m ] + '&year=' + year;
        importStack.push( url );
    }
    importCount = importStack.length;
    importCSV(); // do the import
}

// do the big import
function updateAll () {
    clearData(); // clear the decks
    var now = new Date;

    // rather brute-force approach to handling not knowing how far back the sales data extends
    // get all the data from 2005 onwards
    // downloading an parsing empty CSV files is quick, and not done often
    for ( var year = 2005; year <= now.getFullYear(); year++ ) {
        for ( var m = 0; m < 12; m++ ) {
            importStack.push( 'http://www.etsy.com/your/listings/sold.csv?month=' + months[ m ] + '&year=' + year );
            if ( now.getFullYear() == year && now.getMonth() == m ) break;
        }
    }
    importingAll = true;
    importCount = importStack.length;
    importCSV(); // do the import
}

// progress message at the top left
function progress () {
    var div = document.getElementById( 'importProgress' );
    if ( div == null ) {
        div = document.createElement( 'div' );
        div.id = 'importProgress';
        div.style.position = 'fixed';
        div.style.left = '2px';
        div.style.top = '2px';
        div.style.border = '1px solid #dddddd';
        div.style.background = '#ffffff';
        div.style.padding = '3px';
        document.body.appendChild( div );
    }
    if ( importStack.length == 0 ) {
        // all done, hide the progress bar
        div.parentNode.removeChild( div );
    } else {
        div.innerHTML = 'Importing sales data file ' + ( importCount - importStack.length + 1 ) + ' of ' + importCount;
    }
}

// download a month's CSV file
function importCSV () {
    progress(); // update progress
    if ( importStack.length == 0 ) {
        // all done
        if ( importingAll ) {
            GM_setValue( username + '_completedImport', true );
        }
        var now = new Date;
        GM_setValue( username + '_lastUpdate', String( now.valueOf() ) );
        if ( ! ui_built ) buildUI(); // if we need to, decorate the page with usefulness
        if ( ! display_ui_built ) buildDisplayUI();
        return;
    }
    // handle the next month on the stack
    var url = importStack.shift();
    GM_xmlhttpRequest( { headers: { Cookie: document.cookie }, method: 'GET', url: url, onload: parseCSV } );
}

// function to convert a date in the CSV file to epoch millisecond time
function convertDate( date ) {
    var bits = date.split( '/' );
    var d = new Date( bits[ 2 ], bits[ 0 ], bits[ 1 ] );
    return String( d.valueOf() ); // string because GM has problems stashing date integers for some reason
}    

// handle the CSV file we've downloaded
// the approach is somewhat naive, because the CSV output is kind of naive
// commas in content appear to be just removed, so we don't have to deal with quoting rules
function parseCSV ( response ) {
    if ( response.status == 200 ) {
        var lines = response.responseText.split( /\n/ );
        var header = lines.shift(); // header line
        var bits = header.split( ',' );
        var num_fields = bits.length; // count how many fields we're supposed to have
        // loop, handling lines
        for ( var l = 0; l < lines.length; l++ ) {
            var line = lines[ l ];
            if ( line == "" ) continue;
            var bits = line.split( ',' );
            // cope with unescaped, unquoted newlines in the output
            while ( bits.length < num_fields ) {
                l++;
                line += lines[ l ];
                bits = line.split( ',' );
            }
            // create our data object
            var data = {
                date: bits[ 0 ],
                date_ms: convertDate( bits[ 0 ] ),
                item: bits[ 1 ],
                buyer: bits[ 2 ],
                quantity: bits[ 3 ],
                price: bits[ 4 ],
                shipping: bits[ 5 ],
                transaction: bits[ 6 ],
                listing: bits[ 7 ],
                ship_to_name: bits[ 10 ],
                addr_1: bits[ 11 ],
                addr_2: bits[ 12 ],
                city: bits[ 13 ],
                state: bits[ 14 ],
                code: bits[ 15 ],
                country: bits[ 16 ],
                order: bits[ 17 ]
            }
            var transaction = GM_getValue( 'transaction_' + data.transaction );
            if ( ! transaction || transaction == '({})' ) {
                // we need to stash the data and add it to the index objects
                GM_setValue( 'transaction_' + data.transaction, uneval( data ) );

                var order = eval( GM_getValue( 'order_' + data.order ) || '({})' );
                order[ data.transaction ] = data.listing;
                GM_setValue( 'order_' + data.order, uneval( order ) );
                
                var buyer_orders = eval( GM_getValue( username + '_orders_' + data.buyer ) || '({})' );
                buyer_orders[ data.order ] = true;
                GM_setValue( username + '_orders_' + data.buyer, uneval( buyer_orders ) );

                var buyer_transactions = eval( GM_getValue( username + '_transactions_' + data.buyer ) || '({})' );
                buyer_transactions[ data.transaction ] = data.listing;
                GM_setValue( username + '_transactions_' + data.buyer, uneval( buyer_transactions ) );

                var transactions = eval( GM_getValue( username + '_transactions' ) || '({})' );
                transactions[ data.transaction ] = true;
                GM_setValue( username + '_transactions', uneval( transactions ) );

                var orders = eval( GM_getValue( username + '_orders' ) || '({})' );
                orders[ data.order ] = true;
                GM_setValue( username + '_orders', uneval( orders ) );

                var buyers = eval( GM_getValue( username + '_buyers' ) || '({})' );
                buyers[ data.buyer ] = true;
                GM_setValue( username + '_buyers', uneval( buyers ) );
            }
            //addListing( data.listing, data.transaction );
        }
        importedFor[ response.finalUrl ] = true; // have data for this month now
        GM_setValue( username + '_importedDataFor', uneval( importedFor ) );
        importCSV(); // next month
    } else {
        alert( 'Error importing data' );
    }
}

// find out who I'm logged in as
function whoami () {
    // 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 span = link;
                while ( span != null && span.nodeName != 'SPAN' ) span = span.previousSibling;
                var text = span.innerHTML;
                var match = text.match( /Happy Birthday (.+)\./ );
                text = ( match ) ? match[ 1 ] : text.substring( text.indexOf( ',' ) + 1 );
                text = text.replace( /^\s*/, '' );
                text = text.replace( /\.\s*$/, '' );
                return text;
            }
         }
     }
}

// utility function to replicate node.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;
    }
}



function buildStats () {
    var transactions = eval( GM_getValue( username + '_transactions' ) || '({})' );
    var sales = { shipping: {}, price: {} };
    for ( var t in transactions ) {
        var transaction = eval( GM_getValue( 'transaction_' + t ) || '({})' );
        if ( transaction.date == null ) continue;
        var bits = transaction.date.split( '/' );
        var year = Number( bits[ 2 ] );
        var month = bits[ 0 ];
        if ( sales.shipping[ year ] == null ) sales.shipping[ year ] = { total: 0 };
        if ( sales.shipping[ year ][ month ] == null ) sales.shipping[ year ][ month ] = 0;
        sales.shipping[ year ][ month ] += Number( transaction.shipping );
        sales.shipping[ year ].total += Number( transaction.shipping );
        if ( sales.price[ year ] == null ) sales.price[ year ] = { total: 0 };
        if ( sales.price[ year ][ month ] == null ) sales.price[ year ][ month ] = 0;
        sales.price[ year ][ month ] += Number( transaction.quantity) * Number( transaction.price );
        sales.price[ year ].total += Number( transaction.quantity) * Number( transaction.price );
    }
    var now = new Date;
    var year = now.getFullYear()
    var month = now.getMonth();

    var table = document.createElement( 'table' );
    var header = table.insertRow( 0 );
    header.insertCell( 0 ).innerHTML = 'Year';
    header.insertCell( 1 ).innerHTML = 'Items';
    header.insertCell( 2 ).innerHTML = 'Shipping';
    header.insertCell( 3 ).innerHTML = 'Total';
    header.style.fontWeight = 'bold';
    for ( var y = 2005; y <= year; y++ ) {
        if ( table.rows.length > 1 && sales.price[ y ] == null ) {
            sales.price[ y ] = { total: 0 };
            sales.shipping[ y ] = { total: 0 };
        }
        if ( sales.price[ y ] ) {
            var tr = document.createElement( 'tr' );
            tr.insertCell( 0 ).innerHTML = y;
            tr.insertCell( 1 ).innerHTML = fmtAmount( sales.price[ y ].total );
            tr.insertCell( 2 ).innerHTML = fmtAmount( sales.shipping[ y ].total );
            tr.insertCell( 3 ).innerHTML = fmtAmount( sales.price[ y ].total + sales.shipping[ y ].total );
            table.appendChild( tr );
        }
        if ( table.rows.length > 1 ) {
            var subRows = [];
            var maxMonth = ( y == year ) ? Number( month ) : 11;
            for ( var m = maxMonth; m >= 0; m-- ) {
                if ( sales.price[ y ][ months[ m ] ] == null ) {
                    sales.price[ y ][ months[ m ] ] = 0;
                    sales.shipping[ y ][ months[ m ] ] = 0;
                }                    
                mRow = document.createElement( 'tr' );
                mRow.insertCell( 0 ).innerHTML = '&nbsp;&nbsp;' + months[ m ];
                mRow.insertCell( 1 ).innerHTML = fmtAmount( sales.price[ y ][ months[ m ] ] );
                mRow.insertCell( 2 ).innerHTML = fmtAmount( sales.shipping[ y ][ months[ m ] ] );
                mRow.insertCell( 3 ).innerHTML = fmtAmount( sales.price[ y ][ months[ m ] ] + sales.shipping[ y ][ months[ m ] ] );
                mRow.style.display = 'none';
                mRow.style.background = '#eeeeee';
                table.appendChild( mRow );
                subRows.push( mRow );
            }
            tr.style.cursor = 'pointer';
            tr.addEventListener( 'click', genToggleStatRows( subRows ), false );
        }
    }
    for ( var r = 1; r < table.rows.length; r++ ) {
        for ( var c = 0; c < 4; c++ ) {
            var cell = table.rows[ r ].cells[ c ];
            if ( c > 0 ) cell.style.textAlign = 'right';
            if ( table.rows[ r ].style.display == 'none' ) {
                cell.style.paddingTop = '0px';
                cell.style.paddingBottom = '0px';
            }
        }
    }
    table.className = 'searchRes';
    return table;
}

function fmtAmount ( num ) {
    if ( num == null ) num = 0;
    num = String( num );
    if ( num.indexOf( '.' ) > -1 ) {
        // this is a hack to work around a stringification error that cropped up
        num = String( Math.round( 100 * num ) / 100 );
    }
    if ( num.indexOf( '.' ) == -1 ) num += '.'
    while ( num.lastIndexOf( '.' ) > ( num.length - 3 ) ) {
        num += '' + '0';
    }
    num = '$' + num;
    return num;
}

function genToggleStatRows ( rows ) {
    return function () { 
        for ( var r = 0; r < rows.length; r++ ) {
            rows[ r ].style.display = ( rows[ r ].style.display == 'none' ) ? '' : 'none';
        }
    }
}





