package org.realityforge.replicant.client.transport;

import arez.Arez;
import arez.ArezContext;
import arez.annotations.Action;
import arez.annotations.Observable;
import arez.annotations.PreDispose;
import java.util.Objects;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import replicant.AreaOfInterest;
import replicant.ChannelAddress;
import replicant.Replicant;
import replicant.spy.DataLoadStatus;
import replicant.spy.DataLoaderConnectFailureEvent;
import replicant.spy.DataLoaderConnectedEvent;
import replicant.spy.DataLoaderDisconnectedEvent;
import replicant.spy.DataLoaderDisconnectFailureEvent;

/**
 * Base class responsible for managing data loading from a particular source.
 *
 * TODO: This class exists as separate base class to make it easier to incrementally test code but should
 * eventually be migrated into AbstractDataLoaderService.
 */
public abstract class AbstractDataLoaderService2
  implements DataLoaderService
{
  private final ReplicantClientSystem _replicantClientSystem;

  @Nonnull
  private State _state = State.DISCONNECTED;

  protected AbstractDataLoaderService2( @Nonnull final ReplicantClientSystem replicantClientSystem )
  {
    _replicantClientSystem = Objects.requireNonNull( replicantClientSystem );
    _replicantClientSystem.registerDataSource( this );
  }

  @PreDispose
  void preDispose()
  {
    _replicantClientSystem.deregisterDataSource( this );
  }

  /**
   * {@inheritDoc}
   */
  @Nonnull
  @Override
  @Observable
  public State getState()
  {
    return _state;
  }

  protected void setState( @Nonnull final State state )
  {
    _state = Objects.requireNonNull( state );
  }

  /**
   * Invoked to fire an event when disconnect has completed.
   */
  protected final void onDisconnected()
  {
    setState( State.DISCONNECTED );
    _replicantClientSystem.updateStatus();
    if ( Replicant.areSpiesEnabled() && Replicant.context().getSpy().willPropagateSpyEvents() )
    {
      Replicant.context().getSpy().reportSpyEvent( new DataLoaderDisconnectedEvent( getSystemType() ) );
    }
  }

  /**
   * Invoked to fire an event when failed to connect.
   */
  protected final void onDisconnectFailure( @Nonnull final Throwable error )
  {
    setState( State.ERROR );
    _replicantClientSystem.updateStatus();
    if ( Replicant.areSpiesEnabled() && Replicant.context().getSpy().willPropagateSpyEvents() )
    {
      Replicant.context().getSpy().reportSpyEvent( new DataLoaderDisconnectFailureEvent( getSystemType(), error ) );
    }
  }

  /**
   * Invoked to fire an event when disconnect has completed.
   */
  protected final void onConnected()
  {
    setState( State.CONNECTED );
    _replicantClientSystem.updateStatus();
    if ( Replicant.areSpiesEnabled() && Replicant.context().getSpy().willPropagateSpyEvents() )
    {
      Replicant.context().getSpy().reportSpyEvent( new DataLoaderConnectedEvent( getSystemType() ) );
    }
  }

  /**
   * Invoked to fire an event when failed to connect.
   */
  protected final void onConnectFailure( @Nonnull final Throwable error )
  {
    setState( State.ERROR );
    _replicantClientSystem.updateStatus();
    if ( Replicant.areSpiesEnabled() && Replicant.context().getSpy().willPropagateSpyEvents() )
    {
      Replicant.context().getSpy().reportSpyEvent( new DataLoaderConnectFailureEvent( getSystemType(), error ) );
    }
  }

  /**
   * Invoked when a change set has been completely processed.
   *
   * @param status the status describing the results of data load.
   */
  protected void onMessageProcessed( @Nonnull final DataLoadStatus status )
  {
    //TODO: Add spy event
  }

  /**
   * Called when a data load has resulted in a failure.
   */
  protected final void onMessageProcessFailure( @Nonnull final Throwable error )
  {
    _replicantClientSystem.disconnectIfPossible( this, error );
    //TODO: Add spy event
  }

  /**
   * Attempted to retrieve data from backend and failed.
   */
  protected final void onMessageReadFailure( @Nonnull final Throwable error )
  {
    _replicantClientSystem.disconnectIfPossible( this, error );
    //TODO: Add spy event
  }

  protected final void onSubscribeStarted( @Nonnull final ChannelAddress address )
  {
    updateAreaOfInterest( address, AreaOfInterest.Status.LOADING, null );
    //TODO: Add spy event
  }

  protected final void onSubscribeCompleted( @Nonnull final ChannelAddress address )
  {
    updateAreaOfInterest( address, AreaOfInterest.Status.LOADED, null );
    //TODO: Add spy event
  }

  protected final void onSubscribeFailed( @Nonnull final ChannelAddress address, @Nonnull final Throwable error )
  {
    updateAreaOfInterest( address, AreaOfInterest.Status.LOAD_FAILED, error );
    //TODO: Add spy event
  }

  protected final void onUnsubscribeStarted( @Nonnull final ChannelAddress address )
  {
    updateAreaOfInterest( address, AreaOfInterest.Status.UNLOADING, null );
    //TODO: Add spy event
  }

  protected final void onUnsubscribeCompleted( @Nonnull final ChannelAddress address )
  {
    updateAreaOfInterest( address, AreaOfInterest.Status.UNLOADED, null );
    //TODO: Add spy event
  }

  protected final void onUnsubscribeFailed( @Nonnull final ChannelAddress address, @Nonnull final Throwable error )
  {
    updateAreaOfInterest( address, AreaOfInterest.Status.UNLOADED, error );
    //TODO: Add spy event
  }

  protected final void onSubscriptionUpdateStarted( @Nonnull final ChannelAddress address )
  {
    updateAreaOfInterest( address, AreaOfInterest.Status.UPDATING, null );
    //TODO: Add spy event
  }

  protected final void onSubscriptionUpdateCompleted( @Nonnull final ChannelAddress address )
  {
    updateAreaOfInterest( address, AreaOfInterest.Status.UPDATED, null );
    //TODO: Add spy event
  }

  protected final void onSubscriptionUpdateFailed( @Nonnull final ChannelAddress address,
                                                   @Nonnull final Throwable error )
  {
    updateAreaOfInterest( address, AreaOfInterest.Status.UPDATE_FAILED, error );
    //TODO: Add spy event
  }

  @Action
  protected void updateAreaOfInterest( @Nonnull final ChannelAddress address,
                                       @Nonnull final AreaOfInterest.Status status,
                                       @Nullable final Throwable error )
  {
    final AreaOfInterest areaOfInterest = Replicant.context().findAreaOfInterestByAddress( address );
    if ( null != areaOfInterest )
    {
      areaOfInterest.updateAreaOfInterest( status, error );
    }
  }

  @Nonnull
  protected final ReplicantClientSystem getReplicantClientSystem()
  {
    return _replicantClientSystem;
  }

  @Nonnull
  protected final ArezContext context()
  {
    return Arez.context();
  }
}

@@ -554,26 +554,31 @@
 erDisconnect
+Failur
 e
-d
 Event;%0Aimpor
@@ -609,31 +609,26 @@
 erDisconnect
-Failur
 e
+d
 Event;%0A%0A/**%0A
@@ -1748,28 +1748,25 @@
 inal void on
-Disc
+C
 onnected()%0A 
@@ -1784,27 +1784,24 @@
 tate( State.
-DIS
 CONNECTED );
@@ -2003,36 +2003,33 @@
 ( new DataLoader
-Disc
+C
 onnectedEvent( g
@@ -2150,20 +2150,17 @@
  void on
-Disc
+C
 onnectFa
@@ -2434,36 +2434,33 @@
 ( new DataLoader
-Disc
+C
 onnectFailureEve
@@ -2592,33 +2592,36 @@
 ed final void on
-C
+Disc
 onnected()%0A  %7B%0A 
@@ -2631,32 +2631,35 @@
 setState( State.
+DIS
 CONNECTED );%0A   
@@ -2857,33 +2857,36 @@
 ( new DataLoader
-C
+Disc
 onnectedEvent( g
@@ -2999,33 +2999,36 @@
 ed final void on
-C
+Disc
 onnectFailure( @
@@ -3294,33 +3294,36 @@
 ( new DataLoader
-C
+Disc
 onnectFailureEve

Reorder methods