package co.smartreceipts.android.persistence.database.controllers.impl;
import android.content.Context;
import android.support.annotation.NonNull;
import com.google.common.base.Preconditions;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import javax.inject.Inject;
import co.smartreceipts.android.analytics.Analytics;
import co.smartreceipts.android.analytics.events.ErrorEvent;
import co.smartreceipts.android.di.scopes.ApplicationScope;
import co.smartreceipts.android.model.Receipt;
import co.smartreceipts.android.model.Trip;
import co.smartreceipts.android.persistence.PersistenceManager;
import co.smartreceipts.android.persistence.database.controllers.ReceiptTableEventsListener;
import co.smartreceipts.android.persistence.database.controllers.TableEventsListener;
import co.smartreceipts.android.persistence.database.controllers.alterations.ReceiptTableActionAlterations;
import co.smartreceipts.android.persistence.database.operations.DatabaseOperationMetadata;
import co.smartreceipts.android.persistence.database.tables.ReceiptsTable;
import co.smartreceipts.android.utils.log.Logger;
import io.reactivex.Observable;
import io.reactivex.Scheduler;
import io.reactivex.Single;
import io.reactivex.disposables.Disposable;
import io.reactivex.exceptions.Exceptions;
import wb.android.storage.StorageManager;
@ApplicationScope
public class ReceiptTableController extends TripForeignKeyAbstractTableController<Receipt> {
private final ReceiptTableActionAlterations mReceiptTableActionAlterations;
private final CopyOnWriteArrayList<ReceiptTableEventsListener> mReceiptTableEventsListeners = new CopyOnWriteArrayList<>();
@Inject
public ReceiptTableController(Context context, PersistenceManager persistenceManager, Analytics analytics,
TripTableController tripTableController) {
this(context, persistenceManager.getDatabase().getReceiptsTable(), persistenceManager.getStorageManager(),
analytics, tripTableController);
}
private ReceiptTableController(@NonNull Context context, @NonNull ReceiptsTable receiptsTable, @NonNull StorageManager storageManager, @NonNull Analytics analytics, @NonNull TripTableController tripTableController) {
this(receiptsTable, new ReceiptTableActionAlterations(context, receiptsTable, storageManager), analytics, tripTableController);
}
private ReceiptTableController(@NonNull ReceiptsTable receiptsTable, @NonNull ReceiptTableActionAlterations receiptTableActionAlterations, @NonNull Analytics analytics, @NonNull TripTableController tripTableController) {
super(receiptsTable, receiptTableActionAlterations, analytics);
mReceiptTableActionAlterations = Preconditions.checkNotNull(receiptTableActionAlterations);
subscribe(new ReceiptRefreshTripPricesListener(Preconditions.checkNotNull(tripTableController)));
}
ReceiptTableController(@NonNull ReceiptsTable receiptsTable, @NonNull ReceiptTableActionAlterations receiptTableActionAlterations, @NonNull Analytics analytics,
@NonNull TripTableController tripTableController, @NonNull Scheduler subscribeOnScheduler, @NonNull Scheduler observeOnScheduler) {
super(receiptsTable, receiptTableActionAlterations, analytics, subscribeOnScheduler, observeOnScheduler);
mReceiptTableActionAlterations = Preconditions.checkNotNull(receiptTableActionAlterations);
subscribe(new ReceiptRefreshTripPricesListener(Preconditions.checkNotNull(tripTableController)));
}
@Override
public synchronized void subscribe(@NonNull TableEventsListener<Receipt> tableEventsListener) {
super.subscribe(tableEventsListener);
if (tableEventsListener instanceof ReceiptTableEventsListener) {
mReceiptTableEventsListeners.add((ReceiptTableEventsListener) tableEventsListener);
}
}
@Override
public synchronized void unsubscribe(@NonNull TableEventsListener<Receipt> tableEventsListener) {
if (tableEventsListener instanceof ReceiptTableEventsListener) {
mReceiptTableEventsListeners.remove(tableEventsListener);
}
super.unsubscribe(tableEventsListener);
}
public synchronized void move(@NonNull final Receipt receiptToMove, @NonNull Trip toTrip) {
Logger.info(this, "#move: {}; {}", receiptToMove, toTrip);
final AtomicReference<Disposable> disposableRef = new AtomicReference<>();
final Disposable disposable = mReceiptTableActionAlterations.preMove(receiptToMove, toTrip)
.flatMap(receipt -> mTripForeignKeyTable.insert(receipt, new DatabaseOperationMetadata()))
.subscribeOn(mSubscribeOnScheduler)
.observeOn(mObserveOnScheduler)
.doOnSuccess(receipt -> {
try {
mReceiptTableActionAlterations.postMove(receiptToMove, receipt);
} catch (Exception e) {
throw Exceptions.propagate(e);
}
})
.subscribe(receipt -> {
Logger.debug(this, "#onMoveSuccess - onSuccess");
for (final ReceiptTableEventsListener tableEventsListener : mReceiptTableEventsListeners) {
tableEventsListener.onMoveSuccess(receiptToMove, receipt);
}
unsubscribeReference(disposableRef);
}, throwable -> {
mAnalytics.record(new ErrorEvent(ReceiptTableController.this, throwable));
Logger.debug(this, "#onMoveFailure - onError");
for (final ReceiptTableEventsListener tableEventsListener : mReceiptTableEventsListeners) {
tableEventsListener.onMoveFailure(receiptToMove, throwable);
}
unsubscribeReference(disposableRef);
});
disposableRef.set(disposable);
compositeDisposable.add(disposable);
}
public synchronized void copy(@NonNull final Receipt receiptToCopy, @NonNull Trip toTrip) {
Logger.info(this, "#move: {}; {}", receiptToCopy, toTrip);
final AtomicReference<Disposable> disposableRef = new AtomicReference<>();
final Disposable disposable = mReceiptTableActionAlterations.preCopy(receiptToCopy, toTrip)
.flatMap(receipt -> mTripForeignKeyTable.insert(receipt, new DatabaseOperationMetadata()))
.doOnSuccess(newReceipt -> {
try {
mReceiptTableActionAlterations.postCopy(receiptToCopy, newReceipt);
} catch (Exception e) {
throw Exceptions.propagate(e);
}
})
.subscribeOn(mSubscribeOnScheduler)
.observeOn(mObserveOnScheduler)
.subscribe(newReceipt -> {
Logger.debug(this, "#onCopySuccess - onSuccess");
for (final ReceiptTableEventsListener tableEventsListener : mReceiptTableEventsListeners) {
tableEventsListener.onCopySuccess(receiptToCopy, newReceipt);
}
unsubscribeReference(disposableRef);
}, throwable -> {
mAnalytics.record(new ErrorEvent(ReceiptTableController.this, throwable));
Logger.debug(this, "#onCopyFailure - onError");
for (final ReceiptTableEventsListener tableEventsListener : mReceiptTableEventsListeners) {
tableEventsListener.onCopyFailure(receiptToCopy, throwable);
}
unsubscribeReference(disposableRef);
});
disposableRef.set(disposable);
compositeDisposable.add(disposable);
}
public synchronized void swapUp(@NonNull final Receipt receiptToSwapUp) {
Logger.info(TAG, "#swapUp: {}", receiptToSwapUp);
final AtomicReference<Disposable> disposableRef = new AtomicReference<>();
final Disposable disposable = mTripForeignKeyTable.get(receiptToSwapUp.getTrip())
.flatMapObservable(receipts -> mReceiptTableActionAlterations.getReceiptsToSwapUp(receiptToSwapUp, receipts))
.flatMapSingle(this::swapReceiptsSingle)
.subscribeOn(mSubscribeOnScheduler)
.observeOn(mObserveOnScheduler)
.subscribe(success -> {
if (success) {
Logger.debug(this, "#onSwapUpSuccess - onNext");
for (final ReceiptTableEventsListener tableEventsListener : mReceiptTableEventsListeners) {
tableEventsListener.onSwapSuccess();
}
} else {
Logger.debug(this, "#onSwapUpFailure - onNext");
for (final ReceiptTableEventsListener tableEventsListener : mReceiptTableEventsListeners) {
tableEventsListener.onSwapFailure(null);
}
}
}, throwable -> {
mAnalytics.record(new ErrorEvent(ReceiptTableController.this, throwable));
Logger.debug(this, "#onSwapUpFailure - onError");
for (final ReceiptTableEventsListener tableEventsListener : mReceiptTableEventsListeners) {
tableEventsListener.onSwapFailure(throwable);
}
unsubscribeReference(disposableRef);
}, () -> {
Logger.debug(this, "#swapUp - onComplete");
unsubscribeReference(disposableRef);
});
disposableRef.set(disposable);
compositeDisposable.add(disposable);
}
public synchronized void swapDown(@NonNull final Receipt receiptToSwapDown) {
Logger.info(this, "#swapDown: {}", receiptToSwapDown);
final AtomicReference<Disposable> disposableRef = new AtomicReference<>();
final Disposable disposable = mTripForeignKeyTable.get(receiptToSwapDown.getTrip())
.flatMapObservable(receipts -> mReceiptTableActionAlterations.getReceiptsToSwapDown(receiptToSwapDown, receipts))
.flatMapSingle(this::swapReceiptsSingle)
.subscribeOn(mSubscribeOnScheduler)
.observeOn(mObserveOnScheduler)
.subscribe(success -> {
if (success) {
Logger.debug(this, "#onSwapDownSuccess - onNext");
for (final ReceiptTableEventsListener tableEventsListener : mReceiptTableEventsListeners) {
tableEventsListener.onSwapSuccess();
}
} else {
Logger.debug(this, "#onSwapDownFailure - onNext");
for (final ReceiptTableEventsListener tableEventsListener : mReceiptTableEventsListeners) {
tableEventsListener.onSwapFailure(null);
}
}
}, throwable -> {
mAnalytics.record(new ErrorEvent(ReceiptTableController.this, throwable));
Logger.debug(this, "#onSwapDownFailure - onError");
for (final ReceiptTableEventsListener tableEventsListener : mReceiptTableEventsListeners) {
tableEventsListener.onSwapFailure(throwable);
}
unsubscribeReference(disposableRef);
}, () -> {
Logger.debug(this, "#swapDown - onComplete");
unsubscribeReference(disposableRef);
});
disposableRef.set(disposable);
compositeDisposable.add(disposable);
}
private Single<Boolean> swapReceiptsSingle(final List<? extends Map.Entry<Receipt, Receipt>> entries) {
return Observable.fromIterable(entries)
.flatMap(entry -> update(entry.getKey(), entry.getValue(), new DatabaseOperationMetadata()))
.filter(receipt -> receipt != null)
.toList()
.flatMap(updatedReceipts -> Single.just(entries.size() == updatedReceipts.size()))
.delay(100, TimeUnit.MILLISECONDS);
// TODO: Refactor this to actually fix the timing issue w/o the delay
// TODO: A real solution should use proper sorting indices instead of relying in the dates (hence why I'm allowing this hack)
}
}