/*
 * Copyright (C) 2017 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.doze;

import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;

import static com.android.systemui.doze.DozeMachine.State.DOZE;
import static com.android.systemui.doze.DozeMachine.State.DOZE_AOD;
import static com.android.systemui.doze.DozeMachine.State.DOZE_AOD_PAUSED;
import static com.android.systemui.doze.DozeMachine.State.DOZE_AOD_PAUSING;
import static com.android.systemui.doze.DozeMachine.State.DOZE_PULSE_DONE;

import android.hardware.biometrics.BiometricAuthenticator;
import android.os.Handler;
import android.util.Log;
import android.view.Display;

import androidx.annotation.Nullable;

import com.android.systemui.biometrics.AuthController;
import com.android.systemui.biometrics.UdfpsController;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.doze.dagger.DozeScope;
import com.android.systemui.doze.dagger.WrappedService;
import com.android.systemui.statusbar.phone.DozeParameters;
import com.android.systemui.keyguard.domain.interactor.DozeInteractor;
import com.android.systemui.user.domain.interactor.SelectedUserInteractor;
import com.android.systemui.util.wakelock.SettableWakeLock;
import com.android.systemui.util.wakelock.WakeLock;

import javax.inject.Inject;
import javax.inject.Provider;

/**
 * Controls the screen when dozing.
 */
@DozeScope
public class DozeScreenState implements DozeMachine.Part {

    private static final boolean DEBUG = DozeService.DEBUG;
    private static final String TAG = "DozeScreenState";

    /**
     * Delay entering low power mode when animating to make sure that we'll have
     * time to move all elements into their final positions while still at 60 fps.
     */
    private static final int ENTER_DOZE_DELAY = 4000;
    /**
     * Hide wallpaper earlier when entering low power mode. The gap between
     * hiding the wallpaper and changing the display mode is necessary to hide
     * the black frame that's inherent to hardware specs.
     */
    public static final int ENTER_DOZE_HIDE_WALLPAPER_DELAY = 2500;

    /**
     * Add an extra delay to the transition to DOZE when udfps is current activated before
     * the display state transitions from ON => DOZE.
     */
    public static final int UDFPS_DISPLAY_STATE_DELAY = 1200;

    private final DozeMachine.Service mDozeService;
    private final Handler mHandler;
    private final Runnable mApplyPendingScreenState = this::applyPendingScreenState;
    private final DozeParameters mParameters;
    private final DozeHost mDozeHost;
    private final AuthController mAuthController;
    private final Provider<UdfpsController> mUdfpsControllerProvider;
    @Nullable private UdfpsController mUdfpsController;
    private final DozeLog mDozeLog;
    private final DozeScreenBrightness mDozeScreenBrightness;
    private final SelectedUserInteractor mSelectedUserInteractor;
    private final DozeInteractor mDozeInteractor;

    private int mPendingScreenState = Display.STATE_UNKNOWN;
    private SettableWakeLock mWakeLock;

    @Inject
    public DozeScreenState(
            @WrappedService DozeMachine.Service service,
            @Main Handler handler,
            DozeHost host,
            DozeParameters parameters,
            WakeLock wakeLock,
            AuthController authController,
            Provider<UdfpsController> udfpsControllerProvider,
            DozeLog dozeLog,
            DozeScreenBrightness dozeScreenBrightness,
            DozeInteractor dozeInteractor,
            SelectedUserInteractor selectedUserInteractor) {
        mDozeService = service;
        mHandler = handler;
        mParameters = parameters;
        mDozeHost = host;
        mWakeLock = new SettableWakeLock(wakeLock, TAG);
        mAuthController = authController;
        mUdfpsControllerProvider = udfpsControllerProvider;
        mDozeLog = dozeLog;
        mDozeScreenBrightness = dozeScreenBrightness;
        mSelectedUserInteractor = selectedUserInteractor;
        mDozeInteractor = dozeInteractor;

        updateUdfpsController();
        if (mUdfpsController == null) {
            mAuthController.addCallback(mAuthControllerCallback);
        }
    }

    private void updateUdfpsController() {
        if (mAuthController.isUdfpsEnrolled(mSelectedUserInteractor.getSelectedUserId())) {
            mUdfpsController = mUdfpsControllerProvider.get();
        } else {
            mUdfpsController = null;
        }
    }

    @Override
    public void destroy() {
        mAuthController.removeCallback(mAuthControllerCallback);
    }

    @Override
    public void transitionTo(DozeMachine.State oldState, DozeMachine.State newState) {
        int screenState = newState.screenState(mParameters);
        mDozeHost.cancelGentleSleep();

        if (newState == DozeMachine.State.FINISH) {
            // Make sure not to apply the screen state after DozeService was destroyed.
            mPendingScreenState = Display.STATE_UNKNOWN;
            mHandler.removeCallbacks(mApplyPendingScreenState);

            applyScreenState(screenState);
            mWakeLock.setAcquired(false);
            return;
        }

        if (screenState == Display.STATE_UNKNOWN) {
            // We'll keep it in the existing state
            return;
        }

        final boolean messagePending = mHandler.hasCallbacks(mApplyPendingScreenState);
        final boolean pulseEnding = oldState == DOZE_PULSE_DONE && newState.isAlwaysOn();
        final boolean turningOn = (oldState == DOZE_AOD_PAUSED || oldState == DOZE)
                && newState.isAlwaysOn();
        final boolean turningOff = (oldState.isAlwaysOn() && newState == DOZE)
                || (oldState == DOZE_AOD_PAUSING && newState == DOZE_AOD_PAUSED);
        final boolean justInitialized = oldState == DozeMachine.State.INITIALIZED;
        if (messagePending || justInitialized || pulseEnding || turningOn) {
            // During initialization, we hide the navigation bar. That is however only applied after
            // a traversal; setting the screen state here is immediate however, so it can happen
            // that the screen turns on again before the navigation bar is hidden. To work around
            // that, wait for a traversal to happen before applying the initial screen state.
            mPendingScreenState = screenState;

            // Delay screen state transitions even longer while animations are running.
            boolean shouldDelayTransitionEnteringDoze = newState == DOZE_AOD
                    && mParameters.shouldDelayDisplayDozeTransition() && !turningOn;

            // Delay screen state transition longer if UDFPS is actively authenticating a fp
            boolean shouldDelayTransitionForUDFPS = newState == DOZE_AOD
                    && mUdfpsController != null && mUdfpsController.isFingerDown();

            if (!messagePending) {
                if (DEBUG) {
                    Log.d(TAG, "Display state changed to " + screenState + " delayed by "
                            + (shouldDelayTransitionEnteringDoze ? ENTER_DOZE_DELAY : 1));
                }

                if (shouldDelayTransitionEnteringDoze) {
                    if (justInitialized) {
                        // If we are delaying transitioning to doze and the display was not
                        // turned on we set it to 'on' first to make sure that the animation
                        // is visible before eventually moving it to doze state.
                        // The display might be off at this point for example on foldable devices
                        // when we switch displays and go to doze at the same time.
                        applyScreenState(Display.STATE_ON);

                        // Restore pending screen state as it gets cleared by 'applyScreenState'
                        mPendingScreenState = screenState;
                    }

                    mHandler.postDelayed(mApplyPendingScreenState, ENTER_DOZE_DELAY);
                } else if (shouldDelayTransitionForUDFPS) {
                    mDozeLog.traceDisplayStateDelayedByUdfps(mPendingScreenState);
                    mHandler.postDelayed(mApplyPendingScreenState, UDFPS_DISPLAY_STATE_DELAY);
                } else {
                    mHandler.post(mApplyPendingScreenState);
                }
            } else if (DEBUG) {
                Log.d(TAG, "Pending display state change to " + screenState);
            }

            if (shouldDelayTransitionEnteringDoze || shouldDelayTransitionForUDFPS) {
                mWakeLock.setAcquired(true);
            }
        } else if (turningOff) {
            mDozeHost.prepareForGentleSleep(() -> applyScreenState(screenState));
        } else {
            applyScreenState(screenState);
        }
    }

    private void applyPendingScreenState() {
        if (mUdfpsController != null && mUdfpsController.isFingerDown()) {
            mDozeLog.traceDisplayStateDelayedByUdfps(mPendingScreenState);
            mHandler.postDelayed(mApplyPendingScreenState, UDFPS_DISPLAY_STATE_DELAY);
            return;
        }

        applyScreenState(mPendingScreenState);
        mPendingScreenState = Display.STATE_UNKNOWN;
    }

    private void applyScreenState(int screenState) {
        if (screenState != Display.STATE_UNKNOWN) {
            if (DEBUG) Log.d(TAG, "setDozeScreenState(" + screenState + ")");
            mDozeService.setDozeScreenState(screenState);
            mDozeInteractor.setDozeScreenState(screenState);
            if (screenState == Display.STATE_DOZE) {
                // If we're entering doze, update the doze screen brightness. We might have been
                // clamping it to the dim brightness during the screen off animation, and we should
                // now change it to the brightness we actually want according to the sensor.
                mDozeScreenBrightness.updateBrightnessAndReady(false /* force */);
            }
            mPendingScreenState = Display.STATE_UNKNOWN;
            mWakeLock.setAcquired(false);
        }
    }

    private final AuthController.Callback mAuthControllerCallback = new AuthController.Callback() {
        @Override
        public void onAllAuthenticatorsRegistered(@BiometricAuthenticator.Modality int modality) {
            if (modality == TYPE_FINGERPRINT) {
                updateUdfpsController();
            }
        }

        @Override
        public void onEnrollmentsChanged(@BiometricAuthenticator.Modality int modality) {
            if (modality == TYPE_FINGERPRINT) {
                updateUdfpsController();
            }
        }
    };
}
