/*
 * 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.doze;

import static android.os.PowerManager.WAKE_REASON_BIOMETRIC;
import static android.os.PowerManager.WAKE_REASON_GESTURE;
import static android.os.PowerManager.WAKE_REASON_LIFT;
import static android.os.PowerManager.WAKE_REASON_PLUGGED_IN;
import static android.os.PowerManager.WAKE_REASON_POWER_BUTTON;
import static android.os.PowerManager.WAKE_REASON_TAP;

import android.annotation.IntDef;
import android.os.PowerManager;
import android.util.TimeUtils;

import androidx.annotation.NonNull;

import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.keyguard.KeyguardUpdateMonitorCallback;
import com.android.systemui.Dumpable;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.keyguard.shared.model.FingerprintAuthenticationStatus;
import com.android.systemui.statusbar.policy.DevicePostureController;

import com.google.errorprone.annotations.CompileTimeConstant;

import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

import javax.inject.Inject;

/**
 * Logs doze events for debugging and triaging purposes. Logs are dumped in bugreports or on demand:
 *      adb shell dumpsys activity service com.android.systemui/.SystemUIService \
 *      dependency DumpController DozeLog,DozeStats
 */
@SysUISingleton
public class DozeLog implements Dumpable {
    private final DozeLogger mLogger;

    private boolean mPulsing;
    private long mSince;
    private SummaryStats mPickupPulseNearVibrationStats;
    private SummaryStats mPickupPulseNotNearVibrationStats;
    private SummaryStats mNotificationPulseStats;
    private SummaryStats mScreenOnPulsingStats;
    private SummaryStats mScreenOnNotPulsingStats;
    private SummaryStats mEmergencyCallStats;
    private SummaryStats[][] mProxStats; // [reason][near/far]

    @Inject
    public DozeLog(
            KeyguardUpdateMonitor keyguardUpdateMonitor,
            DumpManager dumpManager,
            DozeLogger logger) {
        mLogger = logger;
        mSince = System.currentTimeMillis();
        mPickupPulseNearVibrationStats = new SummaryStats();
        mPickupPulseNotNearVibrationStats = new SummaryStats();
        mNotificationPulseStats = new SummaryStats();
        mScreenOnPulsingStats = new SummaryStats();
        mScreenOnNotPulsingStats = new SummaryStats();
        mEmergencyCallStats = new SummaryStats();
        mProxStats = new SummaryStats[TOTAL_REASONS][2];
        for (int i = 0; i < TOTAL_REASONS; i++) {
            mProxStats[i][0] = new SummaryStats();
            mProxStats[i][1] = new SummaryStats();
        }

        if (keyguardUpdateMonitor != null) {
            keyguardUpdateMonitor.registerCallback(mKeyguardCallback);
        }

        dumpManager.registerDumpable("DumpStats", this);
    }

    /**
     * Log debug message to LogBuffer.
     */
    public void d(@CompileTimeConstant String msg) {
        mLogger.log(msg);
    }

    /**
     * Appends pickup wakeup event to the logs
     */
    public void tracePickupWakeUp(boolean withinVibrationThreshold) {
        mLogger.logPickupWakeup(withinVibrationThreshold);
        (withinVibrationThreshold ? mPickupPulseNearVibrationStats
                : mPickupPulseNotNearVibrationStats).append();
    }

    public void traceSetIgnoreTouchWhilePulsing(boolean ignoreTouch) {
        mLogger.logSetIgnoreTouchWhilePulsing(ignoreTouch);
    }

    /**
     * Appends pulse started event to the logs.
     * @param reason why the pulse started
     */
    public void tracePulseStart(@Reason int reason) {
        mLogger.logPulseStart(reason);
        mPulsing = true;
    }

    /**
     * Appends pulse finished event to the logs
     */
    public void tracePulseFinish() {
        mLogger.logPulseFinish();
        mPulsing = false;
    }

    /**
     * Appends pulse event to the logs
     */
    public void traceNotificationPulse() {
        mLogger.logNotificationPulse();
        mNotificationPulseStats.append();
    }

    /**
     * Appends dozing event to the logs. Logs current dozing state when entering/exiting AOD.
     * @param dozing true if dozing, else false
     */
    public void traceDozing(boolean dozing) {
        mLogger.logDozing(dozing);
        mPulsing = false;
    }

    /**
     * Appends dozing event to the logs when dozing has changed in AOD.
     * @param dozing true if we're now dozing, else false
     */
    public void traceDozingChanged(boolean dozing) {
        mLogger.logDozingChanged(dozing);
    }

    /**
     * Appends fling event to the logs
     */
    public void traceFling(boolean expand, boolean aboveThreshold,
            boolean screenOnFromTouch) {
        mLogger.logFling(expand, aboveThreshold, screenOnFromTouch);
    }

    /**
     * Appends emergency call event to the logs
     */
    public void traceEmergencyCall() {
        mLogger.logEmergencyCall();
        mEmergencyCallStats.append();
    }

    /**
     * Appends keyguard bouncer changed event to the logs
     * @param showing true if the keyguard bouncer is showing, else false
     */
    public void traceKeyguardBouncerChanged(boolean showing) {
        mLogger.logKeyguardBouncerChanged(showing);
    }

    /**
     * Appends screen-on event to the logs
     */
    public void traceScreenOn() {
        mLogger.logScreenOn(mPulsing);
        (mPulsing ? mScreenOnPulsingStats : mScreenOnNotPulsingStats).append();
        mPulsing = false;
    }

    /**
     * Appends screen-off event to the logs
     * @param why reason the screen is off
     */
    public void traceScreenOff(int why) {
        mLogger.logScreenOff(why);
    }

    /**
     * Appends missed tick event to the logs
     * @param delay of the missed tick
     */
    public void traceMissedTick(String delay) {
        mLogger.logMissedTick(delay);
    }

    /**
     * Appends time tick scheduled event to the logs
     * @param when time tick scheduled at
     * @param triggerAt time tick trigger at
     */
    public void traceTimeTickScheduled(long when, long triggerAt) {
        mLogger.logTimeTickScheduled(when, triggerAt);
    }

    /**
     * Logs cancelation requests for time ticks
     * @param isPending is an unschedule request pending?
     * @param isTimeTickScheduled is a time tick request scheduled
     */
    public void tracePendingUnscheduleTimeTick(boolean isPending, boolean isTimeTickScheduled) {
        mLogger.logPendingUnscheduleTimeTick(isPending, isTimeTickScheduled);
    }

    /**
     * Appends keyguard visibility change event to the logs
     * @param showing whether the keyguard is now showing
     */
    public void traceKeyguard(boolean showing) {
        mLogger.logKeyguardVisibilityChange(showing);
        if (!showing) mPulsing = false;
    }

    /**
     * Appends doze state changed event to the logs
     * @param state new DozeMachine state
     */
    public void traceState(DozeMachine.State state) {
        mLogger.logDozeStateChanged(state);
    }

    /**
     * Appends doze state changed sent to all DozeMachine parts event to the logs
     * @param state new DozeMachine state
     */
    public void traceDozeStateSendComplete(DozeMachine.State state) {
        mLogger.logStateChangedSent(state);
    }

    /**
     * Appends display state delayed by UDFPS event to the logs
     * @param delayedDisplayState the display screen state that was delayed
     */
    public void traceDisplayStateDelayedByUdfps(int delayedDisplayState) {
        mLogger.logDisplayStateDelayedByUdfps(delayedDisplayState);
    }

    /**
     * Appends display state changed event to the logs
     * @param displayState new DozeMachine state
     * @param afterRequest whether the request has successfully been sent else false for it's
     *                        about to be requested
     */
    public void traceDisplayState(int displayState, boolean afterRequest) {
        mLogger.logDisplayStateChanged(displayState, afterRequest);
    }

    /**
     * Appends wake-display event to the logs.
     * @param wake if we're waking up or sleeping.
     */
    public void traceWakeDisplay(boolean wake, @Reason int reason) {
        mLogger.logWakeDisplay(wake, reason);
    }

    /**
     * Appends proximity result event to the logs
     * @param near true if near, else false
     * @param reason why proximity result was triggered
     */
    public void traceProximityResult(boolean near, long millis, @Reason int reason) {
        mLogger.logProximityResult(near, millis, reason);
        mProxStats[reason][near ? 0 : 1].append();
    }

    /**
     * Appends usudfps screen-off pulse event to the logs.
     * @param state the state while usudfps screen-off pulse event raised.
     */
    public void traceUltrasonicScreenOffPulseEvent(FingerprintAuthenticationStatus state) {
        if (state == null) return;
        mLogger.logUltrasonicScreenOffPulseEvent(state);
    }

    /**
     * Appends fingerprint screen-off pulse event to the logs.
     * @param state the authentication state that caused the pulse request
     * @param failureCount number of fingerprint failures during this AOD session
     */
    public void traceFingerprintScreenOffPulseEvent(FingerprintAuthenticationStatus state,
            int failureCount) {
        if (state == null) return;
        mLogger.logFingerprintScreenOffPulseEvent(state, failureCount);
    }

    @Override
    public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
        synchronized (DozeLog.class) {
            pw.print("  Doze summary stats (for ");
            TimeUtils.formatDuration(System.currentTimeMillis() - mSince, pw);
            pw.println("):");
            mPickupPulseNearVibrationStats.dump(pw, "Pickup pulse (near vibration)");
            mPickupPulseNotNearVibrationStats.dump(pw, "Pickup pulse (not near vibration)");
            mNotificationPulseStats.dump(pw, "Notification pulse");
            mScreenOnPulsingStats.dump(pw, "Screen on (pulsing)");
            mScreenOnNotPulsingStats.dump(pw, "Screen on (not pulsing)");
            mEmergencyCallStats.dump(pw, "Emergency call");
            for (int i = 0; i < TOTAL_REASONS; i++) {
                final String reason = reasonToString(i);
                mProxStats[i][0].dump(pw, "Proximity near (" + reason + ")");
                mProxStats[i][1].dump(pw, "Proximity far (" + reason + ")");
            }
        }
    }

    /**
     * Appends doze updates due to a posture change.
     */
    public void tracePostureChanged(
            @DevicePostureController.DevicePostureInt int posture,
            String partUpdated
    ) {
        mLogger.logPostureChanged(posture, partUpdated);
    }

    /**
     * Appends pulse dropped event to logs
     */
    public void tracePulseDropped(String from, DozeMachine.State state) {
        mLogger.logPulseDropped(from, state);
    }

    /**
     * Appends sensor event dropped event to logs
     */
    public void traceSensorEventDropped(@Reason int pulseReason, String reason) {
        mLogger.logSensorEventDropped(pulseReason, reason);
    }

    /**
     * Appends pulsing event to logs.
     */
    public void tracePulseEvent(String pulseEvent, boolean dozing, int pulseReason) {
        mLogger.logPulseEvent(pulseEvent, dozing, DozeLog.reasonToString(pulseReason));
    }

    /**
     * Appends pulse dropped event to logs
     * @param reason why the pulse was dropped
     */
    public void tracePulseDropped(String reason) {
        mLogger.logPulseDropped(reason);
    }

    /**
     * Appends pulse touch displayed by prox sensor event to logs
     * @param disabled
     */
    public void tracePulseTouchDisabledByProx(boolean disabled) {
        mLogger.logPulseTouchDisabledByProx(disabled);
    }

    /**
     * Appends sensor triggered event to logs
     * @param reason why the sensor was triggered
     */
    public void traceSensor(@Reason int reason) {
        mLogger.logSensorTriggered(reason);
    }

    /**
     * Appends the doze state that was suppressed to the doze event log
     * @param suppressedState The {@link DozeMachine.State} that was suppressed
     * @param reason what suppressed always on
     */
    public void traceAlwaysOnSuppressed(DozeMachine.State suppressedState, String reason) {
        mLogger.logAlwaysOnSuppressed(suppressedState, reason);
    }

    /**
     * Appends reason why doze immediately ended.
     */
    public void traceImmediatelyEndDoze(String reason) {
        mLogger.logImmediatelyEndDoze(reason);
    }

    /**
     * Appends power save changes that may cause a new doze state
     * @param powerSaveActive true if power saving is active
     * @param nextState the state that we'll transition to
     */
    public void tracePowerSaveChanged(boolean powerSaveActive, DozeMachine.State nextState) {
        mLogger.logPowerSaveChanged(powerSaveActive, nextState);
    }

    /**
     * Appends an event on AOD suppression change
     * @param suppressed true if AOD is being suppressed
     * @param nextState the state that we'll transition to
     */
    public void traceAlwaysOnSuppressedChange(boolean suppressed, DozeMachine.State nextState) {
        mLogger.logAlwaysOnSuppressedChange(suppressed, nextState);
    }

    /**
     * Appends new AOD screen brightness to logs
     * @param brightness display brightness setting between {@link PowerManager#BRIGHTNESS_MIN} and
     *                   {@link PowerManager#BRIGHTNESS_MAX}
     * @param afterRequest whether the request has successfully been sent else false for it's
     *                        about to be requested
     */
    public void traceDozeScreenBrightness(float brightness, boolean afterRequest) {
        mLogger.logDozeScreenBrightness(brightness, afterRequest);
    }

    /**
    * Appends new AOD dimming scrim opacity to logs
    * @param scrimOpacity
     */
    public void traceSetAodDimmingScrim(float scrimOpacity) {
        mLogger.logSetAodDimmingScrim(scrimOpacity);
    }

    /**
     * Appends new AOD wallpaper dimming scrim opacity to logs
     * @param scrimOpacity
     */
    public void traceSetAodWallpaperDimmingScrim(float scrimOpacity) {
        mLogger.logSetAodWallpaperDimmingScrim(scrimOpacity);
    }

    /**
     * Appends sensor attempted to register and whether it was a successful registration.
     */
    public void traceSensorRegisterAttempt(String sensorName, boolean successfulRegistration) {
        mLogger.logSensorRegisterAttempt(sensorName, successfulRegistration);
    }

    /**
     * Appends sensor attempted to unregister and whether it was successfully unregistered.
     */
    public void traceSensorUnregisterAttempt(String sensorInfo, boolean successfullyUnregistered) {
        mLogger.logSensorUnregisterAttempt(sensorInfo, successfullyUnregistered);
    }

    /**
     * Appends sensor attempted to unregister and whether it was successfully unregistered
     * with a reason the sensor is being unregistered.
     */
    public void traceSensorUnregisterAttempt(String sensorInfo, boolean successfullyUnregistered,
            String reason) {
        mLogger.logSensorUnregisterAttempt(sensorInfo, successfullyUnregistered, reason);
    }

    /**
     * Appends the event of skipping a sensor registration since it's already registered.
     */
    public void traceSkipRegisterSensor(String sensorInfo) {
        mLogger.logSkipSensorRegistration(sensorInfo);
    }

    /**
     * Appends a plugin sensor was registered or unregistered event.
     */
    public void tracePluginSensorUpdate(boolean registered) {
        if (registered) {
            mLogger.log("register plugin sensor");
        } else {
            mLogger.log("unregister plugin sensor");
        }
    }

    private class SummaryStats {
        private int mCount;

        public void append() {
            mCount++;
        }

        public void dump(PrintWriter pw, String type) {
            if (mCount == 0) return;
            pw.print("    ");
            pw.print(type);
            pw.print(": n=");
            pw.print(mCount);
            pw.print(" (");
            final double perHr = (double) mCount / (System.currentTimeMillis() - mSince)
                    * 1000 * 60 * 60;
            pw.print(perHr);
            pw.print("/hr)");
            pw.println();
        }
    }

    private final KeyguardUpdateMonitorCallback mKeyguardCallback =
            new KeyguardUpdateMonitorCallback() {
        @Override
        public void onEmergencyCallAction() {
            traceEmergencyCall();
        }

        @Override
        public void onKeyguardBouncerFullyShowingChanged(boolean fullyShowing) {
            traceKeyguardBouncerChanged(fullyShowing);
        }

        @Override
        public void onStartedWakingUp() {
            traceScreenOn();
        }

        @Override
        public void onFinishedGoingToSleep(int why) {
            traceScreenOff(why);
        }

        @Override
        public void onKeyguardVisibilityChanged(boolean visible) {
            traceKeyguard(visible);
        }
    };

    /**
     * Converts the reason (integer) to a user-readable string
     */
    public static String reasonToString(@Reason int pulseReason) {
        switch (pulseReason) {
            case PULSE_REASON_INTENT: return "intent";
            case PULSE_REASON_NOTIFICATION: return "notification";
            case PULSE_REASON_SENSOR_SIGMOTION: return "sigmotion";
            case REASON_SENSOR_PICKUP: return "pickup";
            case REASON_SENSOR_DOUBLE_TAP: return "doubletap";
            case PULSE_REASON_SENSOR_LONG_PRESS: return "longpress";
            case PULSE_REASON_DOCKING: return "docking";
            case PULSE_REASON_SENSOR_WAKE_REACH: return "reach-wakelockscreen";
            case REASON_SENSOR_WAKE_UP_PRESENCE: return "presence-wakeup";
            case REASON_SENSOR_TAP: return "tap";
            case REASON_SENSOR_UDFPS_LONG_PRESS: return "udfps";
            case REASON_SENSOR_QUICK_PICKUP: return "quickPickup";
            case PULSE_REASON_FINGERPRINT_ACTIVATED: return "fingerprint-triggered";
            case PULSE_REASON_FINGERPRINT_PULSE_SHOW_AUTH_UI: return "fingerprint-pulse-auth-ui";
            case PULSE_REASON_FINGERPRINT_PULSE_SHOW_FULL_UI: return "fingerprint-pulse-full-ui";
            case PULSE_REASON_MINMODE: return "minmode";
            default: throw new IllegalArgumentException("invalid reason: " + pulseReason);
        }
    }

    /**
     * Converts {@link Reason} to {@link PowerManager.WakeReason}.
     */
    public static @PowerManager.WakeReason int getPowerManagerWakeReason(@Reason int wakeReason) {
        switch (wakeReason) {
            case REASON_SENSOR_DOUBLE_TAP:
            case REASON_SENSOR_TAP:
                return WAKE_REASON_TAP;
            case REASON_SENSOR_PICKUP:
                return WAKE_REASON_LIFT;
            case REASON_SENSOR_UDFPS_LONG_PRESS:
                return WAKE_REASON_BIOMETRIC;
            case PULSE_REASON_DOCKING:
                return WAKE_REASON_PLUGGED_IN;
            case PULSE_REASON_MINMODE:
                return WAKE_REASON_POWER_BUTTON;
            default:
                return WAKE_REASON_GESTURE;
        }
    }

    @Retention(RetentionPolicy.SOURCE)
    @IntDef({PULSE_REASON_NONE, PULSE_REASON_INTENT, PULSE_REASON_NOTIFICATION,
            PULSE_REASON_SENSOR_SIGMOTION, REASON_SENSOR_PICKUP, REASON_SENSOR_DOUBLE_TAP,
            PULSE_REASON_SENSOR_LONG_PRESS, PULSE_REASON_DOCKING, REASON_SENSOR_WAKE_UP_PRESENCE,
            PULSE_REASON_SENSOR_WAKE_REACH, REASON_SENSOR_TAP,
            PULSE_REASON_FINGERPRINT_PULSE_SHOW_AUTH_UI,
            PULSE_REASON_FINGERPRINT_PULSE_SHOW_FULL_UI,
            REASON_SENSOR_UDFPS_LONG_PRESS,
            REASON_SENSOR_QUICK_PICKUP,
            PULSE_REASON_FINGERPRINT_ACTIVATED, PULSE_REASON_MINMODE
    })
    public @interface Reason {}
    public static final int PULSE_REASON_NONE = -1;
    public static final int PULSE_REASON_INTENT = 0;
    public static final int PULSE_REASON_NOTIFICATION = 1;
    public static final int PULSE_REASON_SENSOR_SIGMOTION = 2;
    public static final int REASON_SENSOR_PICKUP = 3;
    public static final int REASON_SENSOR_DOUBLE_TAP = 4;
    public static final int PULSE_REASON_SENSOR_LONG_PRESS = 5;
    public static final int PULSE_REASON_DOCKING = 6;
    public static final int REASON_SENSOR_WAKE_UP_PRESENCE = 7;
    public static final int PULSE_REASON_SENSOR_WAKE_REACH = 8;
    public static final int REASON_SENSOR_TAP = 9;
    public static final int REASON_SENSOR_UDFPS_LONG_PRESS = 10;
    public static final int REASON_SENSOR_QUICK_PICKUP = 11;
    public static final int PULSE_REASON_FINGERPRINT_ACTIVATED = 12;
    public static final int PULSE_REASON_FINGERPRINT_PULSE_SHOW_AUTH_UI = 13;
    public static final int PULSE_REASON_FINGERPRINT_PULSE_SHOW_FULL_UI = 14;
    public static final int PULSE_REASON_MINMODE = 15;

    public static final int TOTAL_REASONS = 16;
}
