var g_timerSync = null;
var g_mapTransactionColumnNames = null;
var g_listQueuedDownloads = new Array();
var g_listQueuedTallyDownloads = new Array();
var g_mapQueuedOutings = {};
var g_numRemaining = -1;
var g_numTotal = -1;
var g_activeSync = false;
var g_warned = false;
var g_pollInterval = MIN_POLL_INTERVAL;
var g_syncFailures = 0;

function scrubQueuedTransactions()
{
    setLocalStorage( "db-queued-transactions", null );
};

function scrubQueuedTallies()
{
    setLocalStorage( "db-queued-tallies", null );
};

function scrubQueuedOutings()
{
    setLocalStorage( "db-queued-outings", null );
};

function syncPoll()
{
    if ( g_pollInterval < MAX_POLL_INTERVAL )
    {
        g_pollInterval = POLL_SCALE * g_pollInterval;
//console_log( "increasing poll interval to " + g_pollInterval );
    }

    workerSync( false, "poll" );
};

/**
 * This is an implementation of pageToWorkerInterface method
 */
function workerPause()
{
    window.clearTimeout( g_timerSync );
};

function restartPollInterval()
{
   if ( g_pollInterval > MIN_POLL_INTERVAL )
   {
//console_log( "resetting poll interval to " + MIN_POLL_INTERVAL );
       g_pollInterval = MIN_POLL_INTERVAL;
       restartSyncPoll();
   }
};

function restartSyncPoll()
{
    workerPause();

    g_timerSync = window.setTimeout( syncPoll, g_pollInterval );
};

/**
 * This is an implementation of pageToWorkerInterface method
 */
function workerReset()
{
    setLastSyncTimestamp( 0 );
    setLastTallySyncTimestamp( 0 );
    setLastOutingSyncTimestamp( 0 );
};

/**
 * This is an implementation of pageToWorkerInterface method
 */
function workerSync( bFromGUI, strReason )
{
    if ( ! g_activeSync )
    {
        g_activeSync = true;

        doSync( bFromGUI, strReason );
    }
    else
        console_warn( "ignoring re-entrant sync (" + strReason + ")" );
};

function doUploadOutings()
{
    if ( g_isEmbedded ) 
        return;

    var strListOutings = getLocalStorage( "db-queued-outings" );       // get it as a JSON string... i.e., don't parse

    //if ( strListOutings != null )
        //console_log( "queued outings = '" + strListOutings + "'" );

    if ( strListOutings != null )
    {
        var bAnyNew = false;
        var listOutings = JSON.parse( strListOutings );
        for ( var iOuting = 0; iOuting < listOutings.length; iOuting++ )
        {
            var outing = listOutings[iOuting];
            if ( outing.id < 0 ) // new?
                bAnyNew = true;
        }

        //var strData = "action=upload&sectionid=" + getSection() + "&outings=" + strListOutings;
        //console_log( "about to upload outings '" + strData + "'" );

        jQuery.post(
            BADGES_SYNC,
            {
                action: "upload",
                sectionid: getSection(),
                outings: strListOutings,
                uid: getLoginID()
            },
            function( data ) 
            {
                setLastOutingSyncTimestamp( data.otimestamp - (bAnyNew?1:0) );
                scrubQueuedOutings();
            },
            "json"
        );
    }
};

function doUploadTransactions()
{
    var listTransactions = getLocalStorage( "db-queued-transactions" );     // get it as a JSON string... i.e., don't parse

    if ( ! g_isEmbedded && listTransactions != null )
    {
        var strData = "action=upload&sectionid=" + getSection() + "&transactions=" + listTransactions;
        //console_log( "about to upload transactions '" + strData + "'" );

        jQuery.post(
            BADGES_SYNC,
            {
                action: "upload",
                sectionid: getSection(),
                transactions: listTransactions,
                uid: getLoginID()
            },
            function( data ) 
            {
                scrubQueuedTransactions();
                syncComplete();
                setLastSyncTimestamp( data.timestamp );
            },
            "json"
        );
    }
    else
    {
        syncComplete();
    }
};

function doUploadTallies()
{
    var listTallies = getLocalStorage( "db-queued-tallies" );     // get it as a JSON string... i.e., don't parse

    if ( ! g_isEmbedded && listTallies != null )
    {
        var strData = "action=upload&sectionid=" + getSection() + "&tallies=" + listTallies;
        //console_log( "about to upload transactions '" + strData + "'" );

        jQuery.post(
            BADGES_SYNC,
            {
                action: "upload",
                sectionid: getSection(),
                tallies: listTallies,
                uid: getLoginID()
            },
            function( data ) 
            {
                scrubQueuedTallies();
                setLastTallySyncTimestamp( data.timestamp );
            },
            "json"
        );
    }
};

function doDownloadTransactions( strReason )
{
    var nRecords = 20000;

    var strData = "action=download&sectionid=" + getSection() + "&numrecords=" + nRecords + "&oafter=" + getLastOutingSyncTimestamp();
    // don't get the transactions if we're embedded
    if ( ! g_isEmbedded )
        strData += "&after=" + getLastSyncTimestamp() + "&tafter=" + getLastTallySyncTimestamp(); 
    
    // don't trigger a presence update if the poll interval has been growing (due to inactivity)
    if ( g_pollInterval <= POLL_SCALE * MIN_POLL_INTERVAL )
        strData += "&uid=" + getLoginID();

    // if we've never logged in as a leader, then we don't need to download _all_ the data.
    if ( getLocalStorage( "multiyouth" ) == null && getRole() == "p" )
    {
        var strYouthIDs = "";
        for ( var youthID in g_mapYouthNames )
             strYouthIDs = strYouthIDs + "," + youthID;
        strYouthIDs = strYouthIDs.substring(1);       // strip off initial delimiter
        console_log( "constraining sync to '" + strYouthIDs + "'" );
        strData = strData + "&youthid=" + strYouthIDs;
    }

    //console_trace( "about to download '" + strData + "' (" + strReason + ")" );

    jQuery.ajax({
        url: BADGES_SYNC,
        data: strData,
        dataType: 'json',
        timeout: 10000,
        success: function( data ) 
        {
            if ( data != null )
            {
                g_syncFailures = 0;
                parseDownloadTransactions( data );
            }
            else
            {
                console_warn( "success but no data, active = " + $.active );
                handleSyncFailure();
            }
        },
        error: function( XMLHttpRequest, textStatus, errorThrown )
        {
            console_warn( "ajax error '" + textStatus + "' (" + errorThrown + ")" );
            handleSyncFailure();
        }
    });
};

function handleSyncFailure()
{
    if ( g_syncFailures++ >= 3 )        // three strikes
    {
        if ( ! g_warned )
        {
            setOnline( false );
            alert( "Sync error: continuing with old data in off-line mode" );
        }
        g_warned = true;
        g_syncFailures = 0;
    }
    else
    {
        g_activeSync = false;       // we're not complete, but we need to re-issue the request
        cancelSync();
        scheduleSync( "re-attempt sync " + g_syncFailures, 5000 );
    }
};

function syncComplete()
{
    g_activeSync = false;
    g_numTotal = -1;
    g_numRemaining = 0;

    syncCallback();
    restartSyncPoll();
};

function doSync( bFromGUI, strReason )
{
    workerPause();

    if ( ! isOnline() )
    {
        console_log( "ignoring off-line attempt to sync transactions (" + strReason + ")" );
        syncComplete();
        return;
    }

    if ( ! getSection() )
    {
        console_warn( "wowot! no sectionID (" + strReason + ")" );
        syncComplete();
        return;
    }

    doDownloadTransactions( "doSync (" + strReason + ")" );
};

function parseDownloadTransactions( data )
{
    //console_log( "data '" + JSON.stringify( data ) + "'" );
    if ( data == null )
    {
        console_warn( "nothing to parse" );
        syncComplete();         // probably, it was a server/ajax error
        return 0;
    }
     
    if ( data.columns != null )          // any data returned from ajax call?
    {
        //console_log( "rows = " + data.rows.length + ", remaining = " + data.remaining + ", timestamp = " + data.timestamp );
        if ( g_numTotal <= 0 )
        {
            if ( data.rows != null )
            {
                g_numTotal = data.rows.length + data.remaining;
                if ( data.trows != null )
                    g_numTotal += data.trows.length;
                if ( data.outings != null )
                    g_numTotal += size( data.outings );
            }
            else
                g_numTotal = 0;
            g_numRemaining = g_numTotal;
            var strMessage = getLastSyncTimestamp() > 0 ? "Sync in progress..." : "Initial sync in progress...";
            // normally we put up the progress bar whenever there is anything coming down the pipe.  However,
            // we always end up doing a sync after creating a new event, so this becomes distracting.  Similarly,
            // if the last event updates were deletions, then you can end up with a bunch of sync's upon startup
            // (since the otimestamp is freshly derived from extant events).  
            // So, we add the caveat that if the sync just resulted in ONLY outings being downloaded, don't bother
            // showing the progress bar.  Yes, this means that if another scouter updates a couple of events (and
            // NOTHING else), then you'll miss seeing the sync message, but this is a lesser evil than showing the
            // sync after every event creation, or upon every startup if after an event deletion.
            if ( data.outings != null && size( data.outings ) == g_numTotal )
                console_log( "skipping progress bar for " + g_numTotal + " events" ); // don't invoke the progress bar
            else if ( g_numTotal > 0 )
                openLightBox( { size: "medium", text: strMessage + "<table cellspacing=0 class='progress'><tr><td style='width:0;'><img height=8 width=1 src='images/blank.gif' style='border:none;padding:0;margin:0;'/></td><td></td></tr></table><div>Update <span>0</span> of " + g_numTotal + "</div>" } );
        }

        g_mapTransactionColumnNames = new Array();
        for ( var iColumn = 0; iColumn < data.columns.length; iColumn++ )
            g_mapTransactionColumnNames.push( data.columns[iColumn] );

        g_mapTallyColumnNames = new Array();
        if ( data.tcolumns !== undefined )
        {
            for ( var iColumn = 0; iColumn < data.tcolumns.length; iColumn++ )
                g_mapTallyColumnNames.push( data.tcolumns[iColumn] );
            // KLUDGE, remove the null from row jk
        }

        if ( size( g_mapTransactionColumnNames ) > 0 )
            g_listQueuedDownloads = g_listQueuedDownloads.concat( data.rows );

        if ( size( g_mapTallyColumnNames ) > 0 )
            g_listQueuedTallyDownloads = g_listQueuedTallyDownloads.concat( data.trows );

        if ( data.outings != null )
            $.extend( g_mapQueuedOutings, data.outings );

        // defer this a bit, so the ajax call can be deemed to have finished
        window.setTimeout( processQueuedDownloads, 100 );
    }

    else if ( g_numRemaining > 0 )
    {
        console_warn( "no result, but num remaining was " + g_numRemaining );
        syncComplete();
    }

    // don't update the timestamp if we didn't ask for any data
    else if ( ! g_isEmbedded )
    {
        setLastSyncTimestamp( data.timestamp );
        setLastTallySyncTimestamp( data.timestamp );
        doUploadTallies();
        doUploadTransactions();
    }
    else
    {
        syncCallback();
        workerPause;
    }
};

function addOuting( outing, mozOutingCombos )
{
    //console_log( "outing " + outing.id + " = '" + JSON.stringify( outing ) + "'" );
   
    // this could be an outing that was just inserted.  in this case, we need 
    // to blow away the temporary instantiation of the outing (indicated by an ID
    // that is the negative of the timestamp)
    delete mozOutingCombos["o-" + outing.timestamp]; 
    delete g_mapOutings["-" + outing.timestamp]; 
    if ( getCurrentEvent() == -outing.timestamp )
    {
        console_log( "replacing view-edit outingid '" + getCurrentEvent() + "' with '" + outing.id + "'" );
        setCurrentEvent( outing.id );
    }

    if ( outing.date < 0 )
    {
        delete mozOutingCombos["o" + outing.id]; 
        deleteOuting( outing.id ); 
    }
    else
    {
        mozOutingCombos["o" + outing.id] = outing;
        setOuting( outing );
    }

    return mozOutingCombos
};

function processQueuedOutingsBlock( nBlockSize )
{
    var nProcessed = 0;

    var timestamp = getLastOutingSyncTimestamp();
    var timestampOrig = timestamp;
    var mozOutingCombos = getJsonTableMap( 'db-outing-combos' );

    for ( var key in g_mapQueuedOutings )
    {
        var outing = g_mapQueuedOutings[key];

        // we deal with blocks of records, but if we get a bunch of outings that were all 
        // synced at the same time, then we continue to read even though we've nominally collected enough
        if ( nProcessed >= nBlockSize )
            if ( outing.timestamp != timestamp )        // is the timestmap different from the last event?
                break;           // okay, we're finally done

        outing.youth = outing.youth2;
        delete outing.youth2;

        delete g_mapQueuedOutings[key];

        if ( outing.timestamp > timestamp )
            timestamp = outing.timestamp;

        //console_log( "add outing '" + JSON.stringify( outing ) ) ;
        addOuting( outing, mozOutingCombos );

        nProcessed++;
    }

    if ( nProcessed > 0 )
    {
        setLocalStorage( 'db-outing-combos', JSON.stringify( mozOutingCombos ) );
        if ( size( g_mapQueuedOutings ) == 0 )
        {
            updateEventSelector();
            doEventReportRefresh();
            doAttendanceReportRefresh();

            if ( hasUpPoint( "upcoming" ) )
                buildUpcomingEvents( false );
        }
    }

    g_numRemaining -= nProcessed;
    syncProgress( g_numTotal - g_numRemaining, g_numTotal );

    if ( timestamp > timestampOrig )
        setLastOutingSyncTimestamp( timestamp );

    return size( g_mapQueuedOutings );
};

function processQueuedDownloads()
{
    var nSize = g_listQueuedDownloads.length;
    var nBlockSize = nSize <= 100 ? 10 : (nSize <= 1000 ? 50 : 100 );

    window.setTimeout( function() {
        var timestamp = getLastSyncTimestamp();
        var nLeft = processQueuedDownloadsBlock( nBlockSize );
        if ( nLeft > 0 )
            processQueuedDownloads();       // do this again after a brief pause...

        else
        {
            nLeft = processQueuedTallyDownloadsBlock( nBlockSize );
            if ( nLeft > 0 )
                processQueuedDownloads();       // do this again after a brief pause...
            else
            {
                nLeft = processQueuedOutingsBlock( 10 );
                if ( nLeft > 0 )
                    processQueuedDownloads();       // do this again after a brief pause...

                else
                {
                    if ( g_isEmbedded )
                    {
                        syncCallback();
                        workerPause();         // no more sync's after the initial one
                    }

                    doUploadTransactions();
                    doUploadTallies();
                    doUploadOutings();
                }
            }
        }
    }, 100 );
};

function processQueuedDownloadsBlock( nBlockSize )
{
    if ( g_listQueuedDownloads.length == 0 )
        return 0;

    var nProcessed = 0;
    var timestamp = getLastSyncTimestamp();
    var timestampOrig = timestamp;
    var nBookmarkedChanged = 0;
    var nReadyChanged = 0;
    var nAwardedChanged = 0;
    var nCompletedChanged = 0;
    var setUpdatedYouth = {};

    while ( g_listQueuedDownloads.length > 0 )
    {
        var row = g_listQueuedDownloads[0];       // pop this chunk off the queue

        var transaction = {};
        for ( var iColumn = 0; iColumn < row.length; iColumn++ )
            transaction[g_mapTransactionColumnNames[iColumn]] = row[iColumn];

        var reqID = transaction.requirementid;
        var flag = transaction.flag;
        var inserted = transaction.inserted;
        var youthID = transaction.youthid;

        setUpdatedYouth[youthID] = 1;

        if ( nProcessed >= nBlockSize )
            if ( inserted != timestamp )
                break;           // okay, we're finally done

        g_listQueuedDownloads.shift();       // pop this chunk off the queue

        if ( inserted > timestamp )
            timestamp = inserted;

        if ( flag == FLAG_COMPLETION )
        {
            // dealing with a requirement
            if ( transaction.state == STATE_SET )
                nCompletedChanged += addRequirementComplete( youthID, reqID, transaction.notes, false, false, transaction.timestamp, transaction.updatedby, false );
            else
                nCompletedChanged += clearRequirementComplete( youthID, reqID, transaction.notes, false, false );
        }
        else if ( flag == FLAG_AWARDED )
        {
            // dealing with a badge (stored in 'requirementid')
            if ( transaction.state == STATE_SET )
                nAwardedChanged += addAwarded( youthID, reqID, transaction.notes, transaction.timestamp, transaction.updatedby, false );
            else
                nAwardedChanged += clearAwarded( youthID, reqID, transaction.notes, false );
        }
        else if ( flag == FLAG_READY )
        {
            // dealing with a requirement
            if ( transaction.state == STATE_SET )
                nReadyChanged += addReady( youthID, reqID, transaction.notes, transaction.timestamp, transaction.updatedby, false );
            else
                nReadyChanged += clearReady( youthID, reqID, transaction.notes, false );
        }
        else
        {
            // dealing with a bookmark
            if ( transaction.state == STATE_SET )
                nBookmarkedChanged += addBookmark( youthID, reqID, transaction.notes, transaction.timestamp, transaction.updatedby, false );
            else
                nBookmarkedChanged += clearBookmark( youthID, reqID, transaction.notes, false );
        }

        nProcessed++;
    }

    g_numRemaining -= nProcessed;
    syncProgress( g_numTotal - g_numRemaining, g_numTotal );

    if ( nBookmarkedChanged > 0 )
        setLocalStorage( 'db-bookmarks', JSON.stringify( g_setBookmarkedRequirements ) );

    if ( nAwardedChanged > 0 )
        setLocalStorage( 'db-awarded', JSON.stringify( g_setAwardedBadges ) );

    if ( nReadyChanged > 0 )
        setLocalStorage( 'db-ready', JSON.stringify( g_setReadyRequirements ) );

    if ( nCompletedChanged > 0 )
        setLocalStorage( 'db-completed-requirements', JSON.stringify( g_setMarkedRequirements ) );
        
    // erase the fact that we've scorecarded these youth 
    // we'll re-scorecard whoever is necessary in sync complete
    for ( var youthID in setUpdatedYouth )
        unscorecard( youthID );

    if ( timestamp > timestampOrig )
        setLastSyncTimestamp( timestamp )

    return g_listQueuedDownloads.length;
};

function processQueuedTallyDownloadsBlock( nBlockSize )
{
    if ( g_listQueuedTallyDownloads.length == 0 )
        return 0;

    var nProcessed = 0;
    var timestamp = getLastTallySyncTimestamp();
    var timestampOrig = timestamp;
    var setUpdatedYouth = {};
    var nTallyChanged = 0;

    while ( g_listQueuedTallyDownloads.length > 0 )
    {
        var row = g_listQueuedTallyDownloads[0];       // pop this chunk off the queue

        var tally = {};
        for ( var iColumn = 0; iColumn < row.length; iColumn++ )
            tally[g_mapTallyColumnNames[iColumn]] = row[iColumn];

        var reqID = tally.requirementid;
        var nCount = tally.count;
        var inserted = tally.inserted;
        var youthID = tally.youthid;

        setUpdatedYouth[youthID] = 1;

        if ( nProcessed >= nBlockSize )
            if ( inserted != timestamp )
                break;           // okay, we're finally done

        g_listQueuedTallyDownloads.shift();       // pop this chunk off the queue

        if ( inserted > timestamp )
            timestamp = inserted;

        nTallyChanged += addTally( youthID, reqID, nCount, tally.notes, false, false, tally.timestamp, tally.updatedby, false );

        nProcessed++;
    }

    g_numRemaining -= nProcessed;
    syncProgress( g_numTotal - g_numRemaining, g_numTotal );

    if ( nTallyChanged > 0 )
        setLocalStorage( 'db-tallies', JSON.stringify( g_setTallies ) );
        
    // erase the fact that we've scorecarded these youth 
    // we'll re-scorecard whoever is necessary in sync complete
    for ( var youthID in setUpdatedYouth )
        unscorecard( youthID );

    if ( timestamp > timestampOrig )
        setLastTallySyncTimestamp( timestamp )

    return g_listQueuedTallyDownloads.length;
};

