/*
 * Copyright (C) 2022 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.content.res.Configuration.UI_MODE_TYPE_CAR;

import android.hardware.display.AmbientDisplayConfiguration;
import android.os.PowerManager;
import android.text.TextUtils;

import com.android.systemui.doze.dagger.DozeScope;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.phone.BiometricUnlockController;

import java.io.PrintWriter;

import javax.inject.Inject;

import dagger.Lazy;

/**
 * Handles suppressing doze on:
 * 1. INITIALIZED, don't allow dozing at all when:
 *      - in CAR_MODE, in this scenario the device is asleep and won't listen for any triggers
 *      to wake up. In this state, no UI shows. Unlike other conditions, this suppression is only
 *      temporary and stops when the device exits CAR_MODE
 *      - device is NOT provisioned
 *      - there's a pending authentication
 * 2. PowerSaveMode active
 *      - no always-on-display (DOZE_AOD)
 *      - continues to allow doze triggers (DOZE, DOZE_REQUEST_PULSE)
 * 3. Suppression changes from the PowerManager API. See {@link PowerManager#suppressAmbientDisplay}
 *      and {@link DozeHost#isAlwaysOnSuppressed()}.
 *      - no always-on-display (DOZE_AOD)
 *      - allow doze triggers (DOZE), but disallow notifications (handled by {@link DozeTriggers})
 *      - See extra check in {@link DozeMachine} to guarantee device never enters always-on states
 */
@DozeScope
public class DozeSuppressor implements DozeMachine.Part {

    private DozeMachine mMachine;
    private final DozeHost mDozeHost;
    private final AmbientDisplayConfiguration mConfig;
    private final DozeLog mDozeLog;
    private final Lazy<BiometricUnlockController> mBiometricUnlockControllerLazy;
    private final UserTracker mUserTracker;

    private boolean mIsCarModeEnabled = false;

    @Inject
    public DozeSuppressor(
            DozeHost dozeHost,
            AmbientDisplayConfiguration config,
            DozeLog dozeLog,
            Lazy<BiometricUnlockController> biometricUnlockControllerLazy,
            UserTracker userTracker) {
        mDozeHost = dozeHost;
        mConfig = config;
        mDozeLog = dozeLog;
        mBiometricUnlockControllerLazy = biometricUnlockControllerLazy;
        mUserTracker = userTracker;
    }

    @Override
    public void onUiModeTypeChanged(int newUiModeType) {
        boolean isCarModeEnabled = newUiModeType == UI_MODE_TYPE_CAR;
        if (mIsCarModeEnabled == isCarModeEnabled) {
            return;
        }
        mIsCarModeEnabled = isCarModeEnabled;
        // Do not handle the event if doze machine is not initialized yet.
        // It will be handled upon initialization.
        if (mMachine.isUninitializedOrFinished()) {
            return;
        }
        if (mIsCarModeEnabled) {
            handleCarModeStarted();
        } else {
            handleCarModeExited();
        }
    }

    @Override
    public void setDozeMachine(DozeMachine dozeMachine) {
        mMachine = dozeMachine;
    }

    @Override
    public void transitionTo(DozeMachine.State oldState, DozeMachine.State newState) {
        switch (newState) {
            case INITIALIZED:
                mDozeHost.addCallback(mHostCallback);
                checkShouldImmediatelyEndDoze();
                checkShouldImmediatelySuspendDoze();
                break;
            case FINISH:
                destroy();
                break;
            default:
        }
    }

    @Override
    public void destroy() {
        mDozeHost.removeCallback(mHostCallback);
    }

    private void checkShouldImmediatelySuspendDoze() {
        if (mIsCarModeEnabled) {
            handleCarModeStarted();
        }
    }

    private void checkShouldImmediatelyEndDoze() {
        String reason = null;
        if (!mDozeHost.isProvisioned()) {
            reason = "device_unprovisioned";
        } else if (mBiometricUnlockControllerLazy.get().hasPendingAuthentication()) {
            reason = "has_pending_auth";
        }

        if (!TextUtils.isEmpty(reason)) {
            mDozeLog.traceImmediatelyEndDoze(reason);
            mMachine.requestState(DozeMachine.State.FINISH);
        }
    }

    @Override
    public void dump(PrintWriter pw) {
        pw.println(" isCarModeEnabled=" + mIsCarModeEnabled);
        pw.println(" hasPendingAuth="
                + mBiometricUnlockControllerLazy.get().hasPendingAuthentication());
        pw.println(" isProvisioned=" + mDozeHost.isProvisioned());
        pw.println(" isAlwaysOnSuppressed=" + mDozeHost.isAlwaysOnSuppressed());
        pw.println(" aodPowerSaveActive=" + mDozeHost.isPowerSaveActive());
    }

    private void handleCarModeExited() {
        mDozeLog.traceCarModeEnded();
        mMachine.requestState(mConfig.alwaysOnEnabled(mUserTracker.getUserId())
                ? DozeMachine.State.DOZE_AOD : DozeMachine.State.DOZE);
    }

    private void handleCarModeStarted() {
        mDozeLog.traceCarModeStarted();
        mMachine.requestState(DozeMachine.State.DOZE_SUSPEND_TRIGGERS);
    }

    private final DozeHost.Callback mHostCallback = new DozeHost.Callback() {
        @Override
        public void onPowerSaveChanged(boolean active) {
            // handles suppression changes, while DozeMachine#transitionPolicy handles gating
            // transitions to DOZE_AOD
            DozeMachine.State nextState = null;
            if (mDozeHost.isPowerSaveActive()) {
                nextState = DozeMachine.State.DOZE;
            } else if (mMachine.getState() == DozeMachine.State.DOZE
                    && mConfig.alwaysOnEnabled(mUserTracker.getUserId())) {
                nextState = DozeMachine.State.DOZE_AOD;
            }

            if (nextState != null) {
                mDozeLog.tracePowerSaveChanged(mDozeHost.isPowerSaveActive(), nextState);
                mMachine.requestState(nextState);
            }
        }

        @Override
        public void onAlwaysOnSuppressedChanged(boolean suppressed) {
            // handles suppression changes, while DozeMachine#transitionPolicy handles gating
            // transitions to DOZE_AOD
            final DozeMachine.State nextState;
            if (mConfig.alwaysOnEnabled(mUserTracker.getUserId()) && !suppressed) {
                nextState = DozeMachine.State.DOZE_AOD;
            } else {
                nextState = DozeMachine.State.DOZE;
            }
            mDozeLog.traceAlwaysOnSuppressedChange(suppressed, nextState);
            mMachine.requestState(nextState);
        }
    };
}
