/*
 * 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 android.annotation.IntDef;
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.dump.DumpManager;

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

import javax.inject.Inject;
import javax.inject.Singleton;

/**
 * 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
 */
@Singleton
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);
    }

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

    /**
     * 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
     * @param dozing true if dozing, else false
     */
    public void traceDozing(boolean dozing) {
        mLogger.logDozing(dozing);
        mPulsing = false;
    }

    /**
     * Appends fling event to the logs
     */
    public void traceFling(boolean expand, boolean aboveThreshold, boolean thresholdNeeded,
            boolean screenOnFromTouch) {
        mLogger.logFling(expand, aboveThreshold, thresholdNeeded, 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);
    }

    /**
     * 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 wake-display event to the logs.
     * @param wake if we're waking up or sleeping.
     */
    public void traceWakeDisplay(boolean wake) {
        mLogger.logWakeDisplay(wake);
    }

    /**
     * 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();
    }

    @Override
    public void dump(@NonNull FileDescriptor fd, @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 pulse dropped event to logs
     */
    public void tracePulseDropped(boolean pulsePending, DozeMachine.State state, boolean blocked) {
        mLogger.logPulseDropped(pulsePending, state, blocked);
    }

    /**
     * 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 doze suppressed event to the logs
     * @param suppressedState The {@link DozeMachine.State} that was suppressed
     */
    public void traceDozeSuppressed(DozeMachine.State suppressedState) {
        mLogger.logDozeSuppressed(suppressedState);
    }

    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 onKeyguardBouncerChanged(boolean bouncer) {
            traceKeyguardBouncerChanged(bouncer);
        }

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

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

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

    /**
     * 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_LOCK_SCREEN: return "wakelockscreen";
            case REASON_SENSOR_WAKE_UP: return "wakeup";
            case REASON_SENSOR_TAP: return "tap";
            default: throw new IllegalArgumentException("invalid reason: " + pulseReason);
        }
    }

    @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,
            PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN, REASON_SENSOR_TAP})
    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 = 7;
    public static final int PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN = 8;
    public static final int REASON_SENSOR_TAP = 9;

    public static final int TOTAL_REASONS = 10;
}
