/*
 * 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 com.android.systemui.statusbar.phone;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.content.Context;
import android.os.Handler;
import android.util.Log;
import android.view.animation.Interpolator;

import com.android.systemui.Interpolators;
import com.android.systemui.doze.DozeHost;
import com.android.systemui.doze.DozeLog;

/**
 * Controller which handles all the doze animations of the scrims.
 */
public class DozeScrimController {
    private static final String TAG = "DozeScrimController";
    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);

    private final DozeParameters mDozeParameters;
    private final Handler mHandler = new Handler();
    private final ScrimController mScrimController;

    private final Context mContext;

    private boolean mDozing;
    private DozeHost.PulseCallback mPulseCallback;
    private int mPulseReason;
    private Animator mInFrontAnimator;
    private Animator mBehindAnimator;
    private float mInFrontTarget;
    private float mBehindTarget;
    private boolean mDozingAborted;
    private boolean mWakeAndUnlocking;
    private boolean mFullyPulsing;

    private float mAodFrontScrimOpacity = 0;

    public DozeScrimController(ScrimController scrimController, Context context) {
        mContext = context;
        mScrimController = scrimController;
        mDozeParameters = new DozeParameters(context);
    }

    public void setDozing(boolean dozing, boolean animate) {
        if (mDozing == dozing) return;
        mDozing = dozing;
        mWakeAndUnlocking = false;
        if (mDozing) {
            mDozingAborted = false;
            abortAnimations();
            mScrimController.setDozeBehindAlpha(1f);
            mScrimController.setDozeInFrontAlpha(
                    mDozeParameters.getAlwaysOn() ? mAodFrontScrimOpacity : 1f);
        } else {
            cancelPulsing();
            if (animate) {
                startScrimAnimation(false /* inFront */, 0f /* target */,
                        NotificationPanelView.DOZE_ANIMATION_DURATION,
                        Interpolators.LINEAR_OUT_SLOW_IN);
                startScrimAnimation(true /* inFront */, 0f /* target */,
                        NotificationPanelView.DOZE_ANIMATION_DURATION,
                        Interpolators.LINEAR_OUT_SLOW_IN);
            } else {
                abortAnimations();
                mScrimController.setDozeBehindAlpha(0f);
                mScrimController.setDozeInFrontAlpha(0f);
            }
        }
    }

    /**
     * Set the opacity of the front scrim when showing AOD1
     *
     * Used to emulate lower brightness values than the hardware supports natively.
     */
    public void setAodDimmingScrim(float scrimOpacity) {
        mAodFrontScrimOpacity = scrimOpacity;
        if (mDozing && !isPulsing() && !mDozingAborted && !mWakeAndUnlocking
                && mDozeParameters.getAlwaysOn()) {
            mScrimController.setDozeInFrontAlpha(mAodFrontScrimOpacity);
        }
    }

    public void setWakeAndUnlocking() {
        // Immediately abort the doze scrims in case of wake-and-unlock
        // for pulsing so the Keyguard fade-out animation scrim can take over.
        if (!mWakeAndUnlocking) {
            mWakeAndUnlocking = true;
            mScrimController.setDozeBehindAlpha(0f);
            mScrimController.setDozeInFrontAlpha(0f);
        }
    }

    /** When dozing, fade screen contents in and out using the front scrim. */
    public void pulse(@NonNull DozeHost.PulseCallback callback, int reason) {
        if (callback == null) {
            throw new IllegalArgumentException("callback must not be null");
        }

        if (!mDozing || mPulseCallback != null) {
            // Pulse suppressed.
            callback.onPulseFinished();
            return;
        }

        // Begin pulse.  Note that it's very important that the pulse finished callback
        // be invoked when we're done so that the caller can drop the pulse wakelock.
        mPulseCallback = callback;
        mPulseReason = reason;
        mScrimController.setDozeInFrontAlpha(1f);
        mHandler.post(mPulseIn);
    }

    /**
     * Aborts pulsing immediately.
     */
    public void abortPulsing() {
        cancelPulsing();
        if (mDozing && !mWakeAndUnlocking) {
            mScrimController.setDozeBehindAlpha(1f);
            mScrimController.setDozeInFrontAlpha(
                    mDozeParameters.getAlwaysOn() && !mDozingAborted ?
                            mAodFrontScrimOpacity : 1f);
        }
    }

    /**
     * Aborts dozing immediately.
     */
    public void abortDoze() {
        mDozingAborted = true;
        abortPulsing();
    }

    public void pulseOutNow() {
        if (mPulseCallback != null && mFullyPulsing) {
            mPulseOut.run();
        }
    }

    public void onScreenTurnedOn() {
        if (isPulsing()) {
            final boolean pickupOrDoubleTap = mPulseReason == DozeLog.PULSE_REASON_SENSOR_PICKUP
                    || mPulseReason == DozeLog.PULSE_REASON_SENSOR_DOUBLE_TAP;
            startScrimAnimation(true /* inFront */, 0f,
                    mDozeParameters.getPulseInDuration(pickupOrDoubleTap),
                    pickupOrDoubleTap ? Interpolators.LINEAR_OUT_SLOW_IN : Interpolators.ALPHA_OUT,
                    mPulseInFinished);
        }
    }

    public boolean isPulsing() {
        return mPulseCallback != null;
    }

    public boolean isDozing() {
        return mDozing;
    }

    public void extendPulse() {
        mHandler.removeCallbacks(mPulseOut);
    }

    private void cancelPulsing() {
        if (DEBUG) Log.d(TAG, "Cancel pulsing");

        if (mPulseCallback != null) {
            mFullyPulsing = false;
            mHandler.removeCallbacks(mPulseIn);
            mHandler.removeCallbacks(mPulseOut);
            mHandler.removeCallbacks(mPulseOutExtended);
            pulseFinished();
        }
    }

    private void pulseStarted() {
        if (mPulseCallback != null) {
            mPulseCallback.onPulseStarted();
        }
    }

    private void pulseFinished() {
        if (mPulseCallback != null) {
            mPulseCallback.onPulseFinished();
            mPulseCallback = null;
        }
    }

    private void abortAnimations() {
        if (mInFrontAnimator != null) {
            mInFrontAnimator.cancel();
        }
        if (mBehindAnimator != null) {
            mBehindAnimator.cancel();
        }
    }

    private void startScrimAnimation(final boolean inFront, float target, long duration,
            Interpolator interpolator) {
        startScrimAnimation(inFront, target, duration, interpolator, null /* endRunnable */);
    }

    private void startScrimAnimation(final boolean inFront, float target, long duration,
            Interpolator interpolator, final Runnable endRunnable) {
        Animator current = getCurrentAnimator(inFront);
        if (current != null) {
            float currentTarget = getCurrentTarget(inFront);
            if (currentTarget == target) {
                return;
            }
            current.cancel();
        }
        ValueAnimator anim = ValueAnimator.ofFloat(getDozeAlpha(inFront), target);
        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float value = (float) animation.getAnimatedValue();
                setDozeAlpha(inFront, value);
            }
        });
        anim.setInterpolator(interpolator);
        anim.setDuration(duration);
        anim.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                setCurrentAnimator(inFront, null);
                if (endRunnable != null) {
                    endRunnable.run();
                }
            }
        });
        anim.start();
        setCurrentAnimator(inFront, anim);
        setCurrentTarget(inFront, target);
    }

    private float getCurrentTarget(boolean inFront) {
        return inFront ? mInFrontTarget : mBehindTarget;
    }

    private void setCurrentTarget(boolean inFront, float target) {
        if (inFront) {
            mInFrontTarget = target;
        } else {
            mBehindTarget = target;
        }
    }

    private Animator getCurrentAnimator(boolean inFront) {
        return inFront ? mInFrontAnimator : mBehindAnimator;
    }

    private void setCurrentAnimator(boolean inFront, Animator animator) {
        if (inFront) {
            mInFrontAnimator = animator;
        } else {
            mBehindAnimator = animator;
        }
    }

    private void setDozeAlpha(boolean inFront, float alpha) {
        if (mWakeAndUnlocking) {
            return;
        }
        if (inFront) {
            mScrimController.setDozeInFrontAlpha(alpha);
        } else {
            mScrimController.setDozeBehindAlpha(alpha);
        }
    }

    private float getDozeAlpha(boolean inFront) {
        return inFront
                ? mScrimController.getDozeInFrontAlpha()
                : mScrimController.getDozeBehindAlpha();
    }

    private final Runnable mPulseIn = new Runnable() {
        @Override
        public void run() {
            if (DEBUG) Log.d(TAG, "Pulse in, mDozing=" + mDozing + " mPulseReason="
                    + DozeLog.pulseReasonToString(mPulseReason));
            if (!mDozing) return;
            DozeLog.tracePulseStart(mPulseReason);

            // Signal that the pulse is ready to turn the screen on and draw.
            pulseStarted();
        }
    };

    private final Runnable mPulseInFinished = new Runnable() {
        @Override
        public void run() {
            if (DEBUG) Log.d(TAG, "Pulse in finished, mDozing=" + mDozing);
            if (!mDozing) return;
            mHandler.postDelayed(mPulseOut, mDozeParameters.getPulseVisibleDuration());
            mHandler.postDelayed(mPulseOutExtended,
                    mDozeParameters.getPulseVisibleDurationExtended());
            mFullyPulsing = true;
        }
    };

    private final Runnable mPulseOutExtended = new Runnable() {
        @Override
        public void run() {
            mHandler.removeCallbacks(mPulseOut);
            mPulseOut.run();
        }
    };

    private final Runnable mPulseOut = new Runnable() {
        @Override
        public void run() {
            mFullyPulsing = false;
            mHandler.removeCallbacks(mPulseOut);
            mHandler.removeCallbacks(mPulseOutExtended);
            if (DEBUG) Log.d(TAG, "Pulse out, mDozing=" + mDozing);
            if (!mDozing) return;
            startScrimAnimation(true /* inFront */, 1,
                    mDozeParameters.getPulseOutDuration(),
                    Interpolators.ALPHA_IN, mPulseOutFinishing);
        }
    };

    private final Runnable mPulseOutFinishing = new Runnable() {
        @Override
        public void run() {
            if (DEBUG) Log.d(TAG, "Pulse out finished");
            DozeLog.tracePulseFinish();
            if (mDozeParameters.getAlwaysOn() && mDozing) {
                // Setting power states can block rendering. For AOD, delay finishing the pulse and
                // setting the power state until the fully black scrim had time to hit the
                // framebuffer.
                mHandler.postDelayed(mPulseOutFinished, 30);
            } else {
                mPulseOutFinished.run();
            }
        }
    };

    private final Runnable mPulseOutFinished = new Runnable() {
        @Override
        public void run() {
            // Signal that the pulse is all finished so we can turn the screen off now.
            DozeScrimController.this.pulseFinished();
            if (mDozeParameters.getAlwaysOn()) {
                mScrimController.setDozeInFrontAlpha(mAodFrontScrimOpacity);
            }
        }
    };
}
