// Copyright 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.content.browser;

import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.view.Choreographer;
import android.view.WindowManager;

import org.chromium.base.TraceEvent;

/**
 * Notifies clients of the default displays's vertical sync pulses.
 * This class works in "burst" mode: once the update is requested, the listener will be
 * called MAX_VSYNC_COUNT times on the vertical sync pulses (on JB) or on every refresh
 * period (on ICS, see below), unless stop() is called.
 * On ICS, VSyncMonitor relies on setVSyncPointForICS() being called to set a reasonable
 * approximation of a vertical sync starting point; see also http://crbug.com/156397.
 */
public class VSyncMonitor {
    private static final long NANOSECONDS_PER_SECOND = 1000000000;
    private static final long NANOSECONDS_PER_MILLISECOND = 1000000;
    private static final long NANOSECONDS_PER_MICROSECOND = 1000;
    public static final int MAX_AUTO_ONVSYNC_COUNT = 5;

    /**
     * VSync listener class
     */
    public interface Listener {
        /**
         * Called very soon after the start of the display's vertical sync period.
         * @param monitor The VSyncMonitor that triggered the signal.
         * @param vsyncTimeMicros Absolute frame time in microseconds.
         */
        public void onVSync(VSyncMonitor monitor, long vsyncTimeMicros);
    }

    private Listener mListener;

    // Display refresh rate as reported by the system.
    private final long mRefreshPeriodNano;

    private boolean mHaveRequestInFlight;
    private int mTriggerNextVSyncCount;

    // Choreographer is used to detect vsync on >= JB.
    private final Choreographer mChoreographer;
    private final Choreographer.FrameCallback mVSyncFrameCallback;

    // On ICS we just post a task through the handler (http://crbug.com/156397)
    private final Runnable mVSyncRunnableCallback;
    private long mGoodStartingPointNano;
    private long mLastPostedNano;

    // If the monitor is activated after having been idle, we synthesize the first vsync to reduce
    // latency.
    private final Handler mHandler = new Handler();
    private final Runnable mSyntheticVSyncRunnable;
    private long mLastVSyncCpuTimeNano;

    public VSyncMonitor(Context context, VSyncMonitor.Listener listener) {
        this(context, listener, true);
    }

    VSyncMonitor(Context context, VSyncMonitor.Listener listener, boolean enableJBVSync) {
        mListener = listener;
        float refreshRate = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE))
                .getDefaultDisplay().getRefreshRate();
        if (refreshRate <= 0) refreshRate = 60;
        mRefreshPeriodNano = (long) (NANOSECONDS_PER_SECOND / refreshRate);
        mTriggerNextVSyncCount = 0;

        if (enableJBVSync && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            // Use Choreographer on JB+ to get notified of vsync.
            mChoreographer = Choreographer.getInstance();
            mVSyncFrameCallback = new Choreographer.FrameCallback() {
                @Override
                public void doFrame(long frameTimeNanos) {
                    TraceEvent.begin("VSync");
                    mGoodStartingPointNano = frameTimeNanos;
                    onVSyncCallback(frameTimeNanos, getCurrentNanoTime());
                    TraceEvent.end("VSync");
                }
            };
            mVSyncRunnableCallback = null;
        } else {
            // On ICS we just hope that running tasks is relatively predictable.
            mChoreographer = null;
            mVSyncFrameCallback = null;
            mVSyncRunnableCallback = new Runnable() {
                @Override
                public void run() {
                    TraceEvent.begin("VSyncTimer");
                    final long currentTime = getCurrentNanoTime();
                    onVSyncCallback(currentTime, currentTime);
                    TraceEvent.end("VSyncTimer");
                }
            };
            mLastPostedNano = 0;
        }
        mSyntheticVSyncRunnable = new Runnable() {
            @Override
            public void run() {
                TraceEvent.begin("VSyncSynthetic");
                final long currentTime = getCurrentNanoTime();
                onVSyncCallback(estimateLastVSyncTime(currentTime), currentTime);
                TraceEvent.end("VSyncSynthetic");
            }
        };
        mGoodStartingPointNano = getCurrentNanoTime();
    }

    /**
     * Returns the time interval between two consecutive vsync pulses in microseconds.
     */
    public long getVSyncPeriodInMicroseconds() {
        return mRefreshPeriodNano / NANOSECONDS_PER_MICROSECOND;
    }

    /**
     * Determine whether a true vsync signal is available on this platform.
     */
    public boolean isVSyncSignalAvailable() {
        return mChoreographer != null;
    }

    /**
     * Stop reporting vsync events. Note that at most one pending vsync event can still be delivered
     * after this function is called.
     */
    public void stop() {
        mTriggerNextVSyncCount = 0;
    }

    /**
     * Unregister the listener.
     * No vsync events will be reported afterwards.
     */
    public void unregisterListener() {
        stop();
        mListener = null;
    }

    /**
     * Request to be notified of the closest display vsync events.
     * Listener.onVSync() will be called soon after the upcoming vsync pulses.
     * It will be called at most MAX_AUTO_ONVSYNC_COUNT times unless requestUpdate() is called.
     */
    public void requestUpdate() {
        mTriggerNextVSyncCount = MAX_AUTO_ONVSYNC_COUNT;
        postCallback();
    }

    /**
     * Set the best guess of the point in the past when the vsync has happened.
     * @param goodStartingPointNano Known vsync point in the past.
     */
    public void setVSyncPointForICS(long goodStartingPointNano) {
        mGoodStartingPointNano = goodStartingPointNano;
    }

    private long getCurrentNanoTime() {
        return System.nanoTime();
    }

    private void onVSyncCallback(long frameTimeNanos, long currentTimeNanos) {
        assert mHaveRequestInFlight;
        mHaveRequestInFlight = false;
        mLastVSyncCpuTimeNano = currentTimeNanos;
        if (mTriggerNextVSyncCount >= 0) {
            mTriggerNextVSyncCount--;
            postCallback();
        }
        if (mListener != null) {
            mListener.onVSync(this, frameTimeNanos / NANOSECONDS_PER_MICROSECOND);
        }
    }

    private void postCallback() {
        if (mHaveRequestInFlight) return;
        mHaveRequestInFlight = true;
        if (postSyntheticVSync()) return;
        if (isVSyncSignalAvailable()) {
            mChoreographer.postFrameCallback(mVSyncFrameCallback);
        } else {
            postRunnableCallback();
        }
    }

    private boolean postSyntheticVSync() {
        final long currentTime = getCurrentNanoTime();
        // Only trigger a synthetic vsync if we've been idle for long enough and the upcoming real
        // vsync is more than half a frame away.
        if (currentTime - mLastVSyncCpuTimeNano < 2 * mRefreshPeriodNano) return false;
        if (currentTime - estimateLastVSyncTime(currentTime) > mRefreshPeriodNano / 2) return false;
        mHandler.post(mSyntheticVSyncRunnable);
        return true;
    }

    private long estimateLastVSyncTime(long currentTime) {
        final long lastRefreshTime = mGoodStartingPointNano +
                ((currentTime - mGoodStartingPointNano) / mRefreshPeriodNano) * mRefreshPeriodNano;
        return lastRefreshTime;
    }

    private void postRunnableCallback() {
        assert !isVSyncSignalAvailable();
        final long currentTime = getCurrentNanoTime();
        final long lastRefreshTime = estimateLastVSyncTime(currentTime);
        long delay = (lastRefreshTime + mRefreshPeriodNano) - currentTime;
        assert delay >= 0 && delay < mRefreshPeriodNano;

        if (currentTime + delay <= mLastPostedNano + mRefreshPeriodNano / 2) {
            delay += mRefreshPeriodNano;
        }

        mLastPostedNano = currentTime + delay;
        if (delay == 0) mHandler.post(mVSyncRunnableCallback);
        else mHandler.postDelayed(mVSyncRunnableCallback, delay / NANOSECONDS_PER_MILLISECOND);
    }
}
