package org.realityforge.replicant.client;

import java.util.LinkedList;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;

/**
 * Class from which to extend to implement a service that loads data from a change set.
 * Data can be loaded by bulk or incrementally and the load can be broken up into several
 * steps to avoid locking a thread such as in GWT.
 */
public abstract class AbstractDataLoaderService
{
  private static final int DEFAULT_CHANGES_TO_PROCESS_PER_TICK = 100;
  private static final int DEFAULT_LINKS_TO_PROCESS_PER_TICK = 100;
  private static final Level LOG_LEVEL = Level.FINEST;
  protected static final Logger LOG = Logger.getLogger( AbstractDataLoaderService.class.getName() );
  @Inject
  private ChangeMapper _changeMapper;
  @Inject
  private EntityChangeBroker _changeBroker;

  private int _lastKnownChangeSet = Integer.MIN_VALUE;

  private final LinkedList<DataLoadAction> _dataLoadActions = new LinkedList<DataLoadAction>();
  private DataLoadAction _currentAction;
  private int _updateCount;
  private int _removeCount;
  private int _linkCount;
  private int _changesToProcessPerTick = DEFAULT_CHANGES_TO_PROCESS_PER_TICK;
  private int _linksToProcessPerTick = DEFAULT_LINKS_TO_PROCESS_PER_TICK;

  protected abstract void onBulkLoadComplete();

  protected abstract void onIncrementalLoadComplete();

  protected abstract void scheduleDataLoad();

  protected final void setChangesToProcessPerTick( final int changesToProcessPerTick )
  {
    _changesToProcessPerTick = changesToProcessPerTick;
  }

  protected final void setLinksToProcessPerTick( final int linksToProcessPerTick )
  {
    _linksToProcessPerTick = linksToProcessPerTick;
  }

  protected final int getLastKnownChangeSet()
  {
    return _lastKnownChangeSet;
  }

  protected abstract ChangeSet parseChangeSet( String rawJsonData );

  protected final void enqueueDataLoad( final boolean isBulkLoad,
                                        final String rawJsonData,
                                        @Nullable final Runnable runnable )
  {
    _dataLoadActions.add( new DataLoadAction( isBulkLoad, rawJsonData, runnable ) );
    scheduleDataLoad();
  }

  protected final boolean progressDataLoad()
  {
    if ( null == _currentAction && _dataLoadActions.isEmpty() )
    {
      if ( LOG.isLoggable( LOG_LEVEL ) )
      {
        LOG.log( LOG_LEVEL, "No data to load. Terminating incremental load process." );
      }
      return false;
    }

    //Step: Retrieve the action from the queue and notify the broker
    if ( null == _currentAction )
    {
      _currentAction = _dataLoadActions.remove();
      if ( LOG.isLoggable( LOG_LEVEL ) )
      {
        LOG.log( LOG_LEVEL, "Action Selected: " + _currentAction );
      }
      if ( _currentAction.isBulkLoad() )
      {
        _changeBroker.disable();
      }
      else
      {
        _changeBroker.pause();
      }
      if ( LOG.isLoggable( Level.INFO ) )
      {
        _updateCount = 0;
        _removeCount = 0;
        _linkCount = 0;
      }
      return true;
    }

    //Step: Parse the json
    final String rawJsonData = _currentAction.getRawJsonData();
    if ( null != rawJsonData )
    {
      if ( LOG.isLoggable( LOG_LEVEL ) )
      {
        LOG.log( LOG_LEVEL, "Parsing JSON: " + _currentAction );
      }
      final ChangeSet changeSet = parseChangeSet( _currentAction.getRawJsonData() );
      _currentAction.setChangeSet( changeSet );
      return true;
    }

    //Step: Process a chunk of changes
    if ( _currentAction.areChangesPending() )
    {
      if ( LOG.isLoggable( LOG_LEVEL ) )
      {
        LOG.log( LOG_LEVEL, "Processing ChangeSet: " + _currentAction );
      }
      Change change;
      for ( int i = 0; i < _changesToProcessPerTick && null != ( change = _currentAction.nextChange() ); i++ )
      {
        final Object entity = _changeMapper.applyChange( change );
        if ( LOG.isLoggable( Level.INFO ) )
        {
          if ( change.isUpdate() )
          {
            _updateCount++;
          }
          else
          {
            _removeCount++;
          }
        }
        _currentAction.changeProcessed( change.isUpdate(), entity );
      }
      return true;
    }

    //Step: Calculate the entities that need to be linked
    if ( !_currentAction.areEntityLinksCalculated() )
    {
      if ( LOG.isLoggable( LOG_LEVEL ) )
      {
        LOG.log( LOG_LEVEL, "Calculating Link list: " + _currentAction );
      }
      _currentAction.calculateEntitiesToLink();
      return true;
    }

    //Step: Process a chunk of links
    if ( _currentAction.areEntityLinksPending() )
    {
      if ( LOG.isLoggable( LOG_LEVEL ) )
      {
        LOG.log( LOG_LEVEL, "Linking Entities: " + _currentAction );
      }
      Linkable linkable;
      for ( int i = 0; i < _linksToProcessPerTick && null != ( linkable = _currentAction.nextEntityToLink() ); i++ )
      {
        linkable.link();
        if ( LOG.isLoggable( Level.INFO ) )
        {
          _linkCount++;
        }
      }
      return true;
    }

    final ChangeSet set = _currentAction.getChangeSet();
    assert null != set;

    //Step: Finalize the change set
    if ( !_currentAction.hasWorldBeenNotified() )
    {
      _currentAction.markWorldAsNotified();
      if ( LOG.isLoggable( LOG_LEVEL ) )
      {
        LOG.log( LOG_LEVEL, "Finalizing action: " + _currentAction );
      }
      _lastKnownChangeSet = set.getSequence();
      if ( _currentAction.isBulkLoad() )
      {
        _changeBroker.enable();
        onBulkLoadComplete();
      }
      else
      {
        _changeBroker.resume();
        onIncrementalLoadComplete();
      }
      return true;
    }
    if ( LOG.isLoggable( Level.INFO ) )
    {
      LOG.info( "ChangeSet " + set.getSequence() + " involved " + _updateCount + " updates, " + _removeCount +
                " removes and " + _linkCount + " links." );
    }

    //Step: Run the post actions
    if ( LOG.isLoggable( LOG_LEVEL ) )
    {
      LOG.log( LOG_LEVEL, "Running post action and cleaning action: " + _currentAction );
    }
    final Runnable runnable = _currentAction.getRunnable();
    if ( null != runnable )
    {
      runnable.run();
    }
    _currentAction = null;
    return true;
  }
}

@@ -1314,33 +1314,24 @@
   protected 
-abstract 
 void onBulkL
@@ -1335,33 +1335,40 @@
 lkLoadComplete()
-;
+%0A  %7B%0A  %7D
 %0A%0A  protected ab
@@ -1361,33 +1361,24 @@
   protected 
-abstract 
 void onIncre
@@ -1397,17 +1397,24 @@
 mplete()
-;
+%0A  %7B%0A  %7D
 %0A%0A  prot

Rework two methods to be empty template methods rather that abstract methods