/*
 * Copyright (C) 2010 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 com.android.mail.utils;

import android.os.Handler;
import android.util.Log;

import java.util.Timer;
import java.util.TimerTask;

/**
 * This class used to "throttle" a flow of events.
 *
 * When {@link #onEvent()} is called, it calls the callback in a certain timeout later.
 * Initially {@link #mMinTimeout} is used as the timeout, but if it gets multiple {@link #onEvent}
 * calls in a certain amount of time, it extends the timeout, until it reaches {@link #mMaxTimeout}.
 *
 * This class is primarily used to throttle content changed events.
 */
public class Throttle {
    public static final boolean DEBUG = false; // Don't submit with true

    public static final int DEFAULT_MIN_TIMEOUT = 150;
    public static final int DEFAULT_MAX_TIMEOUT = 2500;
    // exposed for testing
    public static final int TIMEOUT_EXTEND_INTERVAL = 500;

    private static final String LOG_TAG = LogTag.getLogTag();

    private static Timer TIMER = new Timer();

    private final Clock mClock;
    private final Timer mTimer;

    /** Name of the instance.  Only for logging. */
    private final String mName;

    /** Handler for UI thread. */
    private final Handler mHandler;

    /** Callback to be called */
    private final Runnable mCallback;

    /** Minimum (default) timeout, in milliseconds.  */
    private final int mMinTimeout;

    /** Max timeout, in milliseconds.  */
    private final int mMaxTimeout;

    /** Current timeout, in milliseconds. */
    private int mTimeout;

    /** When {@link #onEvent()} was last called. */
    private long mLastEventTime;

    private MyTimerTask mRunningTimerTask;

    /** Constructor with default timeout */
    public Throttle(String name, Runnable callback, Handler handler) {
        this(name, callback, handler, DEFAULT_MIN_TIMEOUT, DEFAULT_MAX_TIMEOUT);
    }

    /** Constructor that takes custom timeout */
    public Throttle(String name, Runnable callback, Handler handler,int minTimeout,
            int maxTimeout) {
        this(name, callback, handler, minTimeout, maxTimeout, Clock.INSTANCE, TIMER);
    }

    /** Constructor for tests */
    // exposed for testing
    public Throttle(String name, Runnable callback, Handler handler,int minTimeout,
            int maxTimeout, Clock clock, Timer timer) {
        if (maxTimeout < minTimeout) {
            throw new IllegalArgumentException();
        }
        mName = name;
        mCallback = callback;
        mClock = clock;
        mTimer = timer;
        mHandler = handler;
        mMinTimeout = minTimeout;
        mMaxTimeout = maxTimeout;
        mTimeout = mMinTimeout;
    }

    private void debugLog(String message) {
        Log.d(LOG_TAG, "Throttle: [" + mName + "] " + message);
    }

    private boolean isCallbackScheduled() {
        return mRunningTimerTask != null;
    }

    public void cancelScheduledCallback() {
        if (mRunningTimerTask != null) {
            if (DEBUG) debugLog("Canceling scheduled callback");
            mRunningTimerTask.cancel();
            mRunningTimerTask = null;
        }
    }

    // exposed for testing
    public void updateTimeout() {
        final long now = mClock.getTime();
        if ((now - mLastEventTime) <= TIMEOUT_EXTEND_INTERVAL) {
            mTimeout *= 2;
            if (mTimeout >= mMaxTimeout) {
                mTimeout = mMaxTimeout;
            }
            if (DEBUG) debugLog("Timeout extended " + mTimeout);
        } else {
            mTimeout = mMinTimeout;
            if (DEBUG) debugLog("Timeout reset to " + mTimeout);
        }

        mLastEventTime = now;
    }

    public void onEvent() {
        if (DEBUG) debugLog("onEvent");

        updateTimeout();

        if (isCallbackScheduled()) {
            if (DEBUG) debugLog("    callback already scheduled");
        } else {
            if (DEBUG) debugLog("    scheduling callback");
            mRunningTimerTask = new MyTimerTask();
            mTimer.schedule(mRunningTimerTask, mTimeout);
        }
    }

    /**
     * Timer task called on timeout,
     */
    private class MyTimerTask extends TimerTask {
        private boolean mCanceled;

        @Override
        public void run() {
            mHandler.post(new HandlerRunnable());
        }

        @Override
        public boolean cancel() {
            mCanceled = true;
            return super.cancel();
        }

        private class HandlerRunnable implements Runnable {
            @Override
            public void run() {
                mRunningTimerTask = null;
                if (!mCanceled) { // This check has to be done on the UI thread.
                    if (DEBUG) debugLog("Kicking callback");
                    mCallback.run();
                }
            }
        }
    }

    // exposed for testing
    public int getTimeoutForTest() {
        return mTimeout;
    }

    // exposed for testing
    public long getLastEventTimeForTest() {
        return mLastEventTime;
    }
}
