/*
 * 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.os.Handler;
import android.util.Log;

import java.util.HashSet;
import java.util.Set;

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, finishing a task
 * that hasn't been started is also not allowed.</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 Handler mHandler;
    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>();
    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 handler}.
     *
     * @param handler a non-{@code null} handler to use to post runnables to
     * @param listener a non-{@code null} listener where {@code onDrained} will be called
     */
    public TaskDrainer(Handler handler, DrainListener listener) {
        mHandler = checkNotNull(handler, "handler 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 handler}.
     *
     * @param handler a non-{@code null} handler to use to post runnables to
     * @param listener a non-{@code null} listener where {@code onDrained} will be called
     * @param name an optional name used for debug logging
     */
    public TaskDrainer(Handler handler, DrainListener listener, String name) {
        // XX: Probably don't need a handler at all here
        mHandler = checkNotNull(handler, "handler 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");
            }

            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 if it hasn't started. Once finished, a task
     * cannot be finished again (unless it's started again).</p>
     *
     * @param task a key to identify a task
     *
     * @see #taskStarted
     * @see #beginDrain
     *
     * @throws IllegalStateException
     *          If attempting to start a task which is already finished (and not re-started),
     */
    public void taskFinished(T task) {
        synchronized (mLock) {
            if (DEBUG) {
                Log.v(TAG + "[" + mName + "]", "taskFinished " + task);
            }

            if (!mTaskSet.remove(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() {
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                if (DEBUG) {
                    Log.v(TAG + "[" + mName + "]", "onDrained");
                }

                mListener.onDrained();
            }
        });
    }
}
