//                                                                                        -*- coding:utf-8; -*-
// ==UserScript==
// @name         Upwork
// @description  Transforms the Upwork site to suit me
// @author mike@reluk.ca
// @namespace   reluk.ca
// @include /^https?://(?:www\.)?upwork\.com//
// @run-at document-idle
// @resource stylesheet http://reluk.ca/var/cache/userscript/Upwork.css
// @grant GM_addStyle
// @grant GM_getResourceText
// ==/UserScript==
/**
    [ making skills visible
        - in job search results
            ( data-ng-app FindWorkHomeUI | JobSearchUI
        - by default, without clicking *more* to expand the description of each job posting
        - can't do it consistently
            - somehow the FindWorkHomeUI DOM hasn't the relevant data
            < it *is* in the JobSearchUI, deeply embedded somewhere outside the job posting
    */

( function( body )
{


    /** Appends one or more style classes to element e.
      *
      *     @param newNames (String) The names of the new style classes to append, separated by spaces.
      */
    function appendStyleClass( newNames, e )
    {
        var oldNames = e.className;
        e.className = oldNames? oldNames + ' ' + newNames: newNames;
    }



    /** Intitializes this userscript.
      */
    function init()
    {
     // console.log( 'UpworkTown userscript' ); // TEST
        switch( body.getAttribute( 'data-ng-app' ))
        {
            case 'FindWorkHomeUI': // e.g. https://www.upwork.com/ab/find-work/
                toRestartOnFeed = true;
                transformSearchResults_start();
                break;
            case 'jobDetails':
                GM_addStyle( GM_getResourceText( 'stylesheet' )); // from @resource
                scheduledTry = setTimeout( transformJobPost, msTryDelay = 100 );
                break;
            case 'JobSearchUI':    // e.g. https://www.upwork.com/o/jobs/browse/?location=Canada
             // ( function( pushStateWas ) // credit Earnshaw: http://stackoverflow.com/a/7381436/2402790
             // {
             //     history.pushState = function()
             //     {
             //         pushStateWas.apply( window.history, arguments );
             //         console.log( 'pushed location.href=' + location.href  ); // TEST
             //     }
             // })( history.pushState );
             /// fails to detect Upwork's paging, as does replaceState equivalent, ∴ must poll instead:
                var locWas = null;
                setInterval( function() // (re)start the transformation process for each page of results
                {
                    var loc = location.href;
                    if( loc == locWas ) return; // page unchanged

                    locWas = loc;
                    transformSearchResults_start(); // restart
                }, 100/*ms*/ );
             // break;
        }
    }



    /** Duration of pause before the next transformation attempt.
      */
    var msTryDelay;



    /** The currently scheduled transformation attempt, or null if none is scheduled.
      */
    var scheduledTry = null;



    /** Whether to restart the transformation process on detecting that new search results have been fed
      * into the page.
      *
      *     @see #willRestartOnFeed
      */
    var toRestartOnFeed = false;



    /** The name of the town or city whose jobs the user wants to see.
      */
    var townName = 'Toronto';



    /** Tranforms a present document that contains a job posting.
      */
    function transformJobPost()
    {
        scheduledTry = null;
        var anchorSection = document.getElementById( 'jobsProviderAction' );
        if( !anchorSection )
        {
            // It's likely still pending.  The document source encodes the town but no anchor to orient
            // by.  It's introduced later and the document restructured at that time.  Wait for it.
            scheduledTry = setTimeout( transformJobPost, msTryDelay += 500 ); // += avoids over-polling
            return;
        }

        var walker = document.createTreeWalker( anchorSection.parentNode, NodeFilter.SHOW_TEXT, null, false );
        if( !walker ) return;

        var townText = null;
        for( ;; )
        {
            var text = walker.nextNode();
            if( !text ) break;

            if( text.data.indexOf(townName) < 0 ) continue;

            townText = text;
            break;
        }
        var titlePrefix;
        if( townText )
        {
            titlePrefix = 'in: ';
            appendStyleClass( 'inTown', townText.parentNode.parentNode );
        }
        else titlePrefix = 'out: ';
        document.title = titlePrefix + document.title;
     // console.log( 'final msTryDelay=' + msTryDelay  ); // TEST
    }



    /** Attempts to tranform a present document that contains the results of a job search, rescheduling
      * a further attempt if it detects failure.
      */
    function transformSearchResults()
    {
        scheduledTry = null;
        var walker = document.createTreeWalker( body, NodeFilter.SHOW_ELEMENT, null, false );
        if( !walker ) return;

        var xCount = 0; // transformation count
        for( ;; )
        {
            // Make each job posting link open in a new context (tab or window).  This isn't needed if
            // the user remembers to click with the mouse wheel, but I often forget.  So I rely on this
            // transformation, plus Firefox about:config setting browser.tabs.loadInBackground, as per
            // https://support.mozilla.org/en-US/questions/1054179.
            var e = walker.nextNode();
            if( !e ) break;

            if( e.localName != 'a' ) continue; // not a link

            if( e.target == '_blank' ) continue; // nothing to transform, avoid falsifying xCount

            var href = e.href;
            if( !href || href.indexOf('upwork.com/jobs/') < 0 ) continue; // not a job posting link

            e.target = '_blank'; // make it open in a new context, e.g. tab or window
            ++xCount;
        }
        if( toRestartOnFeed && !willRestartOnFeed ) // testing late, when feedDiv may finally exist
        {
            var feedDiv = document.getElementById( 'feed-jobs' );
            if( feedDiv )
            {
                new MutationObserver( function( mutations )
                {
                    transformSearchResults_start(); // restart
                }).observe( feedDiv, { childList: true } );
                willRestartOnFeed = true;
            }
        }
        if( xCount < 4 ) // then search results are likely still pending
        {
            scheduledTry = setTimeout( transformSearchResults, msTryDelay += 500 );
             // retry after a pause, longer each time to avoid over-polling
            return;
        }

     // console.log( 'final msTryDelay=' + msTryDelay  ); // TEST
    }



    /** Starts or restarts the transformation process.
      */
    function transformSearchResults_start()
    {
        if( scheduledTry ) clearTimeout( scheduledTry );
        scheduledTry = setTimeout( transformSearchResults, msTryDelay = 100 );
    }



    /** Whether the transformation process will actually be restarted on detecting that new search
      * results have been fed into the page.
      *
      *     @see #toRestartOnFeed
      */
    var willRestartOnFeed = false;



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

    init();

}( document.body ));


// Note to myself: In order to ease maintenance, my Greasemonkey/Firefox installation of this userscript
// was linked back to the original source files by the following command:
// (n=UpworkTown; ln --force --symbolic ~/var/cache/userscript/$n.user.js ~/.mozilla/firefox/ikouvatt.default/gm_scripts/$n/)