/*
 * Copyright (C) 2014 The Android Open Source Project
 *
 * 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 android.hardware.camera2.utils;

import android.util.Log;

import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.Executor;

import static com.android.internal.util.Preconditions.*;

/**
 * Keep track of multiple concurrent tasks starting and finishing by their key;
 * allow draining existing tasks and figuring out when all tasks have finished
 * (and new ones won't begin).
 *
 * <p>The initial state is to allow all tasks to be started and finished. A task may only be started
 * once, after which it must be finished before starting again. Likewise, a task may only be
 * finished once, after which it must be started before finishing again. It is okay to finish a
 * task before starting it due to different threads handling starting and finishing.</p>
 *
 * <p>When draining begins, no more new tasks can be started. This guarantees that at some
 * point when all the tasks are finished there will be no more collective new tasks,
 * at which point the {@link DrainListener#onDrained} callback will be invoked.</p>
 *
 *
 * @param <T>
 *          a type for the key that will represent tracked tasks;
 *          must implement {@code Object#equals}
 */
public class TaskDrainer<T> {
    /**
     * Fired asynchronously after draining has begun with {@link TaskDrainer#beginDrain}
     * <em>and</em> all tasks that were started have finished.
     */
    public interface DrainListener {
        /** All tasks have fully finished draining; there will be no more pending tasks. */
        public void onDrained();
    }

    private static final String TAG = "TaskDrainer";
    private final boolean DEBUG = false;

    private final Executor mExecutor;
    private final DrainListener mListener;
    private final String mName;

    /** Set of tasks which have been started but not yet finished with #taskFinished */
    private final Set<T> mTaskSet = new HashSet<T>();
    /**
     * Set of tasks which have been finished but not yet started with #taskStarted. This may happen
     * if taskStarted and taskFinished are called from two different threads.
     */
    private final Set<T> mEarlyFinishedTaskSet = new HashSet<T>();
    private final Object mLock = new Object();

    private boolean mDraining = false;
    private boolean mDrainFinished = false;

    /**
     * Create a new task drainer; {@code onDrained} callbacks will be posted to the listener
     * via the {@code executor}.
     *
     * @param executor a non-{@code null} executor to use for listener execution
     * @param listener a non-{@code null} listener where {@code onDrained} will be called
     */
    public TaskDrainer(Executor executor, DrainListener listener) {
        mExecutor = checkNotNull(executor, "executor must not be null");
        mListener = checkNotNull(listener, "listener must not be null");
        mName = null;
    }

    /**
     * Create a new task drainer; {@code onDrained} callbacks will be posted to the listener
     * via the {@code executor}.
     *
     * @param executor a non-{@code null} executor to use for listener execution
     * @param listener a non-{@code null} listener where {@code onDrained} will be called
     * @param name an optional name used for debug logging
     */
    public TaskDrainer(Executor executor, DrainListener listener, String name) {
        mExecutor = checkNotNull(executor, "executor must not be null");
        mListener = checkNotNull(listener, "listener must not be null");
        mName = name;
    }

    /**
     * Mark an asynchronous task as having started.
     *
     * <p>A task cannot be started more than once without first having finished. Once
     * draining begins with {@link #beginDrain}, no new tasks can be started.</p>
     *
     * @param task a key to identify a task
     *
     * @see #taskFinished
     * @see #beginDrain
     *
     * @throws IllegalStateException
     *          If attempting to start a task which is already started (and not finished),
     *          or if attempting to start a task after draining has begun.
     */
    public void taskStarted(T task) {
        synchronized (mLock) {
            if (DEBUG) {
                Log.v(TAG + "[" + mName + "]", "taskStarted " + task);
            }

            if (mDraining) {
                throw new IllegalStateException("Can't start more tasks after draining has begun");
            }

            // Try to remove the task from the early finished set.
            if (!mEarlyFinishedTaskSet.remove(task)) {
                // The task is not finished early. Add it to the started set.
                if (!mTaskSet.add(task)) {
                    throw new IllegalStateException("Task " + task + " was already started");
                }
            }
        }
    }


    /**
     * Mark an asynchronous task as having finished.
     *
     * <p>A task cannot be finished more than once without first having started.</p>
     *
     * @param task a key to identify a task
     *
     * @see #taskStarted
     * @see #beginDrain
     *
     * @throws IllegalStateException
     *          If attempting to finish a task which is already finished (and not started),
     */
    public void taskFinished(T task) {
        synchronized (mLock) {
            if (DEBUG) {
                Log.v(TAG + "[" + mName + "]", "taskFinished " + task);
            }

            // Try to remove the task from started set.
            if (!mTaskSet.remove(task)) {
                // Task is not started yet. Add it to the early finished set.
                if (!mEarlyFinishedTaskSet.add(task)) {
                    throw new IllegalStateException("Task " + task + " was already finished");
                }
            }

            // If this is the last finished task and draining has already begun, fire #onDrained
            checkIfDrainFinished();
        }
    }

    /**
     * Do not allow any more tasks to be started; once all existing started tasks are finished,
     * fire the {@link DrainListener#onDrained} callback asynchronously.
     *
     * <p>This operation is idempotent; calling it more than once has no effect.</p>
     */
    public void beginDrain() {
        synchronized (mLock) {
            if (!mDraining) {
                if (DEBUG) {
                    Log.v(TAG + "[" + mName + "]", "beginDrain started");
                }

                mDraining = true;

                // If all tasks that had started had already finished by now, fire #onDrained
                checkIfDrainFinished();
            } else {
                if (DEBUG) {
                    Log.v(TAG + "[" + mName + "]", "beginDrain ignored");
                }
            }
        }
    }

    private void checkIfDrainFinished() {
        if (mTaskSet.isEmpty() && mDraining && !mDrainFinished) {
            mDrainFinished = true;
            postDrained();
        }
    }

    private void postDrained() {
        mExecutor.execute(() -> {
                if (DEBUG) {
                    Log.v(TAG + "[" + mName + "]", "onDrained");
                }

                mListener.onDrained();
        });
    }
}
