/*
 * 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.DaoException;
import org.greenrobot.greendao.database.Database;

/**
 * An operation that will be enqueued for asynchronous execution.
 *
 * @author Markus
 * @see AsyncSession
 */
// TODO Implement Future<V>
public class AsyncOperation {
    public enum OperationType {
        Insert, InsertInTxIterable, InsertInTxArray, //
        InsertOrReplace, InsertOrReplaceInTxIterable, InsertOrReplaceInTxArray, //
        Update, UpdateInTxIterable, UpdateInTxArray, //
        Delete, DeleteInTxIterable, DeleteInTxArray, //
        DeleteByKey, DeleteAll, //
        TransactionRunnable, TransactionCallable, //
        QueryList, QueryUnique, //
        Load, LoadAll, //
        Count, Refresh
    }

    public static final int FLAG_MERGE_TX = 1;

    /** TODO unused, just an idea */
    public static final int FLAG_STOP_QUEUE_ON_EXCEPTION = 1 << 1;
    public static final int FLAG_TRACK_CREATOR_STACKTRACE = 1 << 2;

    final OperationType type;
    final AbstractDao<Object, Object> dao;
    private final Database database;
    /** Entity, Iterable<Entity>, Entity[], or Runnable. */
    final Object parameter;
    final int flags;

    volatile long timeStarted;
    volatile long timeCompleted;
    private volatile boolean completed;
    volatile Throwable throwable;
    final Exception creatorStacktrace;
    volatile Object result;
    volatile int mergedOperationsCount;

    int sequenceNumber;

    @SuppressWarnings("unchecked")
    /** Either supply dao or database (set other to null). */
    AsyncOperation(OperationType type, AbstractDao<?, ?> dao, Database database, Object parameter, int flags) {
        this.type = type;
        this.flags = flags;
        this.dao = (AbstractDao<Object, Object>) dao;
        this.database = database;
        this.parameter = parameter;
        creatorStacktrace = (flags & FLAG_TRACK_CREATOR_STACKTRACE) != 0 ? new Exception("AsyncOperation was created here") : null;
    }

    public Throwable getThrowable() {
        return throwable;
    }

    public void setThrowable(Throwable throwable) {
        this.throwable = throwable;
    }

    public OperationType getType() {
        return type;
    }

    public Object getParameter() {
        return parameter;
    }

    /**
     * The operation's result after it has completed. Waits until a result is available.
     *
     * @return The operation's result or null if the operation type does not produce any result.
     * @throws {@link AsyncDaoException} if the operation produced an exception
     * @see #waitForCompletion()
     */
    public synchronized Object getResult() {
        if (!completed) {
            waitForCompletion();
        }
        if (throwable != null) {
            throw new AsyncDaoException(this, throwable);
        }
        return result;
    }

    /** @return true if this operation may be merged with others into a single database transaction. */
    public boolean isMergeTx() {
        return (flags & FLAG_MERGE_TX) != 0;
    }

    Database getDatabase() {
        return database != null ? database : dao.getDatabase();
    }

    /**
     * @return true if this operation is mergeable with the given operation. Checks for null, {@link #FLAG_MERGE_TX},
     * and if the database instances match.
     */
    boolean isMergeableWith(AsyncOperation other) {
        return other != null && isMergeTx() && other.isMergeTx() && getDatabase() == other.getDatabase();
    }

    public long getTimeStarted() {
        return timeStarted;
    }

    public long getTimeCompleted() {
        return timeCompleted;
    }

    public long getDuration() {
        if (timeCompleted == 0) {
            throw new DaoException("This operation did not yet complete");
        } else {
            return timeCompleted - timeStarted;
        }
    }

    public boolean isFailed() {
        return throwable != null;
    }

    public boolean isCompleted() {
        return completed;
    }

    /**
     * Waits until the operation is complete. If the thread gets interrupted, any {@link InterruptedException} will be
     * rethrown as a {@link DaoException}.
     *
     * @return Result if any, see {@link #getResult()}
     */
    public synchronized Object waitForCompletion() {
        while (!completed) {
            try {
                wait();
            } catch (InterruptedException e) {
                throw new DaoException("Interrupted while waiting for operation to complete", e);
            }
        }
        return result;
    }

    /**
     * Waits until the operation is 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 the operation completed in the given time frame.
     */
    public synchronized boolean waitForCompletion(int maxMillis) {
        if (!completed) {
            try {
                wait(maxMillis);
            } catch (InterruptedException e) {
                throw new DaoException("Interrupted while waiting for operation to complete", e);
            }
        }
        return completed;
    }

    /** Called when the operation is done. Notifies any threads waiting for this operation's completion. */
    synchronized void setCompleted() {
        completed = true;
        notifyAll();
    }

    public boolean isCompletedSucessfully() {
        return completed && throwable == null;
    }

    /**
     * If this operation was successfully merged with other operation into a single TX, this will give the count of
     * merged operations. If the operation was not merged, it will be 0.
     */
    public int getMergedOperationsCount() {
        return mergedOperationsCount;
    }

    /**
     * Each operation get a unique sequence number when the operation is enqueued. Can be used for efficiently
     * identifying/mapping operations.
     */
    public int getSequenceNumber() {
        return sequenceNumber;
    }

    /** Reset to prepare another execution run. */
    void reset() {
        timeStarted = 0;
        timeCompleted = 0;
        completed = false;
        throwable = null;
        result = null;
        mergedOperationsCount = 0;
    }

    /**
     * The stacktrace is captured using an exception if {@link #FLAG_TRACK_CREATOR_STACKTRACE} was used (null
     * otherwise).
     */
    public Exception getCreatorStacktrace() {
        return creatorStacktrace;
    }
}
