/*
 * Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.greenrobot.greendao.async;

import org.greenrobot.greendao.AbstractDao;
import org.greenrobot.greendao.AbstractDaoSession;
import org.greenrobot.greendao.DaoException;
import org.greenrobot.greendao.async.AsyncOperation.OperationType;
import org.greenrobot.greendao.database.Database;
import org.greenrobot.greendao.query.Query;

import java.util.concurrent.Callable;

/**
 * Asynchronous interface to entity operations. All operations will enqueued a @link {@link AsyncOperation} and return
 * immediately (fine to call on the UI/main thread). The queue will be processed in a (single) background thread. The
 * processing order is the call order of the operations. It's possible to start multiple AsyncSessions that will
 * execute
 * concurrently.
 *
 * @author Markus
 * @see AbstractDaoSession#startAsyncSession()
 */
// Facade to AsyncOperationExecutor: prepares operations and delegates work to AsyncOperationExecutor.
public class AsyncSession {
    private final AbstractDaoSession daoSession;
    private final AsyncOperationExecutor executor;
    private int sessionFlags;

    public AsyncSession(AbstractDaoSession daoSession) {
        this.daoSession = daoSession;
        this.executor = new AsyncOperationExecutor();
    }

    public int getMaxOperationCountToMerge() {
        return executor.getMaxOperationCountToMerge();
    }

    public void setMaxOperationCountToMerge(int maxOperationCountToMerge) {
        executor.setMaxOperationCountToMerge(maxOperationCountToMerge);
    }

    public int getWaitForMergeMillis() {
        return executor.getWaitForMergeMillis();
    }

    public void setWaitForMergeMillis(int waitForMergeMillis) {
        executor.setWaitForMergeMillis(waitForMergeMillis);
    }

    public AsyncOperationListener getListener() {
        return executor.getListener();
    }

    public void setListener(AsyncOperationListener listener) {
        executor.setListener(listener);
    }

    public AsyncOperationListener getListenerMainThread() {
        return executor.getListenerMainThread();
    }

    public void setListenerMainThread(AsyncOperationListener listenerMainThread) {
        executor.setListenerMainThread(listenerMainThread);
    }

    public boolean isCompleted() {
        return executor.isCompleted();
    }

    /**
     * Waits until all enqueued operations are complete. If the thread gets interrupted, any
     * {@link InterruptedException} will be rethrown as a {@link DaoException}.
     */
    public void waitForCompletion() {
        executor.waitForCompletion();
    }

    /**
     * Waits until all enqueued operations are complete, but at most the given amount of milliseconds. If the thread
     * gets interrupted, any {@link InterruptedException} will be rethrown as a {@link DaoException}.
     *
     * @return true if operations completed in the given time frame.
     */
    public boolean waitForCompletion(int maxMillis) {
        return executor.waitForCompletion(maxMillis);
    }

    /** Asynchronous version of {@link AbstractDao#insert(Object)}. */
    public AsyncOperation insert(Object entity) {
        return insert(entity, 0);
    }

    /** Asynchronous version of {@link AbstractDao#insert(Object)}. */
    public AsyncOperation insert(Object entity, int flags) {
        return enqueueEntityOperation(OperationType.Insert, entity, flags);
    }

    /** Asynchronous version of {@link AbstractDao#insertInTx(Object...)}. */
    public <E> AsyncOperation insertInTx(Class<E> entityClass, E... entities) {
        return insertInTx(entityClass, 0, entities);
    }

    /** Asynchronous version of {@link AbstractDao#insertInTx(Object...)}. */
    public <E> AsyncOperation insertInTx(Class<E> entityClass, int flags, E... entities) {
        return enqueEntityOperation(OperationType.InsertInTxArray, entityClass, entities, flags);
    }

    /** Asynchronous version of {@link AbstractDao#insertInTx(Iterable)}. */
    public <E> AsyncOperation insertInTx(Class<E> entityClass, Iterable<E> entities) {
        return insertInTx(entityClass, entities, 0);
    }

    /** Asynchronous version of {@link AbstractDao#insertInTx(Iterable)}. */
    public <E> AsyncOperation insertInTx(Class<E> entityClass, Iterable<E> entities, int flags) {
        return enqueEntityOperation(OperationType.InsertInTxIterable, entityClass, entities, flags);
    }

    /** Asynchronous version of {@link AbstractDao#insertOrReplace(Object)}. */
    public AsyncOperation insertOrReplace(Object entity) {
        return insertOrReplace(entity, 0);
    }

    /** Asynchronous version of {@link AbstractDao#insertOrReplace(Object)}. */
    public AsyncOperation insertOrReplace(Object entity, int flags) {
        return enqueueEntityOperation(OperationType.InsertOrReplace, entity, flags);
    }

    /** Asynchronous version of {@link AbstractDao#insertOrReplaceInTx(Object...)}. */
    public <E> AsyncOperation insertOrReplaceInTx(Class<E> entityClass, E... entities) {
        return insertOrReplaceInTx(entityClass, 0, entities);
    }

    /** Asynchronous version of {@link AbstractDao#insertOrReplaceInTx(Object...)}. */
    public <E> AsyncOperation insertOrReplaceInTx(Class<E> entityClass, int flags, E... entities) {
        return enqueEntityOperation(OperationType.InsertOrReplaceInTxArray, entityClass, entities, flags);
    }

    /** Asynchronous version of {@link AbstractDao#insertOrReplaceInTx(Iterable)}. */
    public <E> AsyncOperation insertOrReplaceInTx(Class<E> entityClass, Iterable<E> entities) {
        return insertOrReplaceInTx(entityClass, entities, 0);
    }

    /** Asynchronous version of {@link AbstractDao#insertOrReplaceInTx(Iterable)}. */
    public <E> AsyncOperation insertOrReplaceInTx(Class<E> entityClass, Iterable<E> entities, int flags) {
        return enqueEntityOperation(OperationType.InsertOrReplaceInTxIterable, entityClass, entities, flags);
    }

    /** Asynchronous version of {@link AbstractDao#update(Object)}. */
    public AsyncOperation update(Object entity) {
        return update(entity, 0);
    }

    /** Asynchronous version of {@link AbstractDao#update(Object)}. */
    public AsyncOperation update(Object entity, int flags) {
        return enqueueEntityOperation(OperationType.Update, entity, flags);
    }

    /** Asynchronous version of {@link AbstractDao#updateInTx(Object...)}. */
    public <E> AsyncOperation updateInTx(Class<E> entityClass, E... entities) {
        return updateInTx(entityClass, 0, entities);
    }

    /** Asynchronous version of {@link AbstractDao#updateInTx(Object...)}. */
    public <E> AsyncOperation updateInTx(Class<E> entityClass, int flags, E... entities) {
        return enqueEntityOperation(OperationType.UpdateInTxArray, entityClass, entities, flags);
    }

    /** Asynchronous version of {@link AbstractDao#updateInTx(Iterable)}. */
    public <E> AsyncOperation updateInTx(Class<E> entityClass, Iterable<E> entities) {
        return updateInTx(entityClass, entities, 0);
    }

    /** Asynchronous version of {@link AbstractDao#updateInTx(Iterable)}. */
    public <E> AsyncOperation updateInTx(Class<E> entityClass, Iterable<E> entities, int flags) {
        return enqueEntityOperation(OperationType.UpdateInTxIterable, entityClass, entities, flags);
    }

    /** Asynchronous version of {@link AbstractDao#delete(Object)}. */
    public AsyncOperation delete(Object entity) {
        return delete(entity, 0);
    }

    /** Asynchronous version of {@link AbstractDao#delete(Object)}. */
    public AsyncOperation delete(Object entity, int flags) {
        return enqueueEntityOperation(OperationType.Delete, entity, flags);
    }

    /** Asynchronous version of {@link AbstractDao#deleteByKey(Object)}. */
    public AsyncOperation deleteByKey(Object key) {
        return deleteByKey(key, 0);
    }

    /** Asynchronous version of {@link AbstractDao#deleteByKey(Object)}. */
    public AsyncOperation deleteByKey(Object key, int flags) {
        return enqueueEntityOperation(OperationType.DeleteByKey, key, flags);
    }

    /** Asynchronous version of {@link AbstractDao#deleteInTx(Object...)}. */
    public <E> AsyncOperation deleteInTx(Class<E> entityClass, E... entities) {
        return deleteInTx(entityClass, 0, entities);
    }

    /** Asynchronous version of {@link AbstractDao#deleteInTx(Object...)}. */
    public <E> AsyncOperation deleteInTx(Class<E> entityClass, int flags, E... entities) {
        return enqueEntityOperation(OperationType.DeleteInTxArray, entityClass, entities, flags);
    }

    /** Asynchronous version of {@link AbstractDao#deleteInTx(Iterable)}. */
    public <E> AsyncOperation deleteInTx(Class<E> entityClass, Iterable<E> entities) {
        return deleteInTx(entityClass, entities, 0);
    }

    /** Asynchronous version of {@link AbstractDao#deleteInTx(Iterable)}. */
    public <E> AsyncOperation deleteInTx(Class<E> entityClass, Iterable<E> entities, int flags) {
        return enqueEntityOperation(OperationType.DeleteInTxIterable, entityClass, entities, flags);
    }

    /** Asynchronous version of {@link AbstractDao#deleteAll()}. */
    public <E> AsyncOperation deleteAll(Class<E> entityClass) {
        return deleteAll(entityClass, 0);
    }

    /** Asynchronous version of {@link AbstractDao#deleteAll()}. */
    public <E> AsyncOperation deleteAll(Class<E> entityClass, int flags) {
        return enqueEntityOperation(OperationType.DeleteAll, entityClass, null, flags);
    }

    /** Asynchronous version of {@link AbstractDaoSession#runInTx(Runnable)}. */
    public AsyncOperation runInTx(Runnable runnable) {
        return runInTx(runnable, 0);
    }

    /** Asynchronous version of {@link AbstractDaoSession#runInTx(Runnable)}. */
    public AsyncOperation runInTx(Runnable runnable, int flags) {
        return enqueueDatabaseOperation(OperationType.TransactionRunnable, runnable, flags);
    }

    /** Asynchronous version of {@link AbstractDaoSession#callInTx(Callable)}. */
    public AsyncOperation callInTx(Callable<?> callable) {
        return callInTx(callable, 0);
    }

    /** Asynchronous version of {@link AbstractDaoSession#callInTx(Callable)}. */
    public AsyncOperation callInTx(Callable<?> callable, int flags) {
        return enqueueDatabaseOperation(OperationType.TransactionCallable, callable, flags);
    }

    /** Asynchronous version of {@link Query#list()}. */
    public AsyncOperation queryList(Query<?> query) {
        return queryList(query, 0);
    }

    /** Asynchronous version of {@link Query#list()}. */
    public AsyncOperation queryList(Query<?> query, int flags) {
        return enqueueDatabaseOperation(OperationType.QueryList, query, flags);
    }

    /** Asynchronous version of {@link Query#unique()}. */
    public AsyncOperation queryUnique(Query<?> query) {
        return queryUnique(query, 0);
    }

    /** Asynchronous version of {@link Query#unique()}. */
    public AsyncOperation queryUnique(Query<?> query, int flags) {
        return enqueueDatabaseOperation(OperationType.QueryUnique, query, flags);
    }

    /** Asynchronous version of {@link AbstractDao#load(Object)}. */
    public AsyncOperation load(Class<?> entityClass, Object key) {
        return load(entityClass, key, 0);
    }

    /** Asynchronous version of {@link AbstractDao#load(Object)}. */
    public AsyncOperation load(Class<?> entityClass, Object key, int flags) {
        return enqueEntityOperation(OperationType.Load, entityClass, key, flags);
    }

    /** Asynchronous version of {@link AbstractDao#loadAll()}. */
    public AsyncOperation loadAll(Class<?> entityClass) {
        return loadAll(entityClass, 0);
    }

    /** Asynchronous version of {@link AbstractDao#loadAll()}. */
    public AsyncOperation loadAll(Class<?> entityClass, int flags) {
        return enqueEntityOperation(OperationType.LoadAll, entityClass, null, flags);
    }

    /** Asynchronous version of {@link AbstractDao#count()}. */
    public AsyncOperation count(Class<?> entityClass) {
        return count(entityClass, 0);
    }

    /** Asynchronous version of {@link AbstractDao#count()}. */
    public AsyncOperation count(Class<?> entityClass, int flags) {
        return enqueEntityOperation(OperationType.Count, entityClass, null, flags);
    }

    /** Asynchronous version of {@link AbstractDao#refresh(Object)}. */
    public AsyncOperation refresh(Object entity) {
        return refresh(entity, 0);
    }

    /** Asynchronous version of {@link AbstractDao#refresh(Object)}. */
    public AsyncOperation refresh(Object entity, int flags) {
        return enqueueEntityOperation(OperationType.Refresh, entity, flags);
    }

    private AsyncOperation enqueueDatabaseOperation(OperationType type, Object param, int flags) {
        Database database = daoSession.getDatabase();
        AsyncOperation operation = new AsyncOperation(type, null, database, param, flags | sessionFlags);
        executor.enqueue(operation);
        return operation;
    }

    private AsyncOperation enqueueEntityOperation(OperationType type, Object entity, int flags) {
        return enqueEntityOperation(type, entity.getClass(), entity, flags);
    }

    private <E> AsyncOperation enqueEntityOperation(OperationType type, Class<E> entityClass, Object param, int flags) {
        AbstractDao<?, ?> dao = daoSession.getDao(entityClass);
        AsyncOperation operation = new AsyncOperation(type, dao, null, param, flags | sessionFlags);
        executor.enqueue(operation);
        return operation;
    }

    /** {@link org.greenrobot.greendao.async.AsyncOperation} flags set for all operations (will be ORed with call flags). */
    public int getSessionFlags() {
        return sessionFlags;
    }

    /** {@link org.greenrobot.greendao.async.AsyncOperation} flags set for all operations (will be ORed with call flags). */
    public void setSessionFlags(int sessionFlags) {
        this.sessionFlags = sessionFlags;
    }
}
