package org.mercury_im.messenger.data.repository;

import org.mercury_im.messenger.core.data.repository.AccountRepository;
import org.mercury_im.messenger.core.util.Optional;
import org.mercury_im.messenger.data.mapping.AccountMapping;
import org.mercury_im.messenger.data.model.AccountModel;
import org.mercury_im.messenger.data.repository.dao.AccountDao;
import org.mercury_im.messenger.entity.Account;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

import javax.inject.Inject;

import io.reactivex.Completable;
import io.reactivex.Maybe;
import io.reactivex.Observable;
import io.reactivex.Single;
import io.requery.Persistable;
import io.requery.query.ResultDelegate;
import io.requery.reactivex.ReactiveEntityStore;
import io.requery.reactivex.ReactiveResult;

public class RxAccountRepository
        extends RequeryRepository
        implements AccountRepository {

    private final AccountMapping accountMapping;
    private final AccountDao dao;

    @Inject
    public RxAccountRepository(ReactiveEntityStore<Persistable> data,
                               AccountMapping accountMapping) {
        super(data);
        this.accountMapping = accountMapping;
        this.dao = new AccountDao(data);
    }

    @Override
    public Single<Account> insertAccount(Account account) {
        return Single.just(account)
                .map(accountMapping::toModel)
                .flatMap(dao::insert)
                .map(model -> accountMapping.toEntity(model, account));
    }

    @Override
    public Observable<Optional<Account>> observeAccount(UUID accountId) {
        return dao.get(accountId).observableResult()
                .map(result -> new Optional<>(result.firstOrNull()))
                .map(accountMapping::toEntity);
    }

    @Override
    public Maybe<Account> getAccount(UUID accountId) {
        return dao.get(accountId).maybe()
                .map(accountMapping::toEntity);
    }

    @Override
    public Observable<Optional<Account>> observeAccountByAddress(String address) {
        return dao.get(address).observableResult()
                .map(result -> new Optional<>(result.firstOrNull()))
                .map(accountMapping::toEntity);
    }

    @Override
    public Maybe<Account> getAccountByAddress(String address) {
        return dao.get(address).maybe()
                .map(accountMapping::toEntity);
    }

    @Override
    public Observable<List<Account>> observeAllAccounts() {
        return dao.getAll().observableResult()
                .map(ResultDelegate::toList)
                .map(this::modelsToEntities);
    }

    @Override
    public Observable<Account> observeAccounts() {
        return dao.getAll().observableResult()
                .flatMap(ReactiveResult::observable)
                .map(accountMapping::toEntity);
    }

    @Override
    public Single<Account> updateAccount(Account account) {
        // Since we cannot access setId() of AccountModel, we have to query the model by ID and update it manually.
        // https://github.com/requery/requery/issues/616#issuecomment-315685460

        return dao.get(account.getId()).maybe().toSingle()
                .map(model -> accountMapping.toModel(account, model))
                .flatMap(updatedModel -> data().update(updatedModel))
                .map(model -> accountMapping.toEntity(model, account));
    }

    @Override
    public Single<Account> upsertAccount(Account account) {
        return dao.get(account.getId()).maybe()
                .switchIfEmpty(
                        Single.just(account).map(accountMapping::toModel).flatMap(dao::insert))
                .map(model -> accountMapping.toModel(account, model))
                .flatMap(data()::update)
                .map(model -> accountMapping.toEntity(model, account));
    }

    @Override
    public Completable deleteAccount(UUID accountId) {
        return dao.delete(accountId).ignoreElement();
    }

    @Override
    public Completable updateRosterVersion(UUID accountId, String rosterVersion) {
        return data().update(AccountModel.class)
                .set(AccountModel.ROSTER_VERSION, rosterVersion)
                .where(AccountModel.ID.eq(accountId))
                .get().single().ignoreElement();
    }

    private List<Account> modelsToEntities(List<AccountModel> models) {
        List<Account> entities = new ArrayList<>(models.size());
        for (AccountModel model : models) {
            entities.add(accountMapping.toEntity(model));
        }
        return entities;
    }
}
