/*
 * Copyright (C) 2023 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.server.wifi;

import android.annotation.NonNull;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiManager.DeviceMobilityState;
import android.util.Log;

import java.util.Arrays;

/**
 * Class for App and client mode RSSI monitoring. It processes the RSSI thresholds for these
 * monitors and enables/disables the monitoring accordingly. It also changes the RSSI polling
 * interval dynamically based on the client mode RSSI monitoring and device mobility state.
 */
public class RssiMonitor {
    private static final String TAG = "RssiMonitor";
    private boolean mVerboseLoggingEnabled = false;

    private final WifiGlobals mWifiGlobals;
    private final WifiThreadRunner mWifiThreadRunner;
    private final WifiInfo mWifiInfo;
    private final WifiNative mWifiNative;
    private final String mInterfaceName;
    private final Runnable mUpdateCapabilityRunnable;
    private final DeviceConfigFacade mDeviceConfigFacade;

    private boolean mEnableClientRssiMonitor = false;
    private boolean mIsPollRssiIntervalOverridden = false;
    private int[] mAppThresholds = {};
    private byte[] mRssiRanges = {};

    public RssiMonitor(WifiGlobals wifiGlobals, WifiThreadRunner wifiThreadRunner,
            WifiInfo wifiInfo, WifiNative wifiNative, String interfaceName,
            Runnable updateCapabilityRunnable, DeviceConfigFacade deviceConfigFacade) {
        mWifiGlobals = wifiGlobals;
        mWifiThreadRunner = wifiThreadRunner;
        mWifiInfo = wifiInfo;
        mWifiNative = wifiNative;
        mInterfaceName = interfaceName;
        mUpdateCapabilityRunnable = updateCapabilityRunnable;
        mDeviceConfigFacade = deviceConfigFacade;
    }

    private void logd(String string) {
        if (mVerboseLoggingEnabled) {
            Log.d(getTag(), string);
        }
    }

    private String getTag() {
        return TAG + "[" + (mInterfaceName == null ? "unknown" : mInterfaceName) + "]";
    }

    class RssiEventHandler implements WifiNative.WifiRssiEventHandler {
        @Override
        public void onRssiThresholdBreached(byte curRssi) {
            if (mVerboseLoggingEnabled) {
                logd("onRssiThresholdBreach event. Cur Rssi = " + curRssi);
            }
            // Corner case: if framework threshold is same as one of the app thresholds,
            // processClientRssiThresholdBreached(curRssi) is called. The framework monitor will
            // be disabled, and the RSSI monitor will be restarted with the app thresholds
            // by calling handleRssiBreachRestartRssiMonitor(). From the App monitor perspective
            // the actions are same as calling handleRssiBreachRestartRssiMonitor(curRssi) directly.
            if (mEnableClientRssiMonitor && curRssi
                    <= mWifiGlobals.getClientRssiMonitorThresholdDbm()) {
                mWifiThreadRunner.post(() -> processClientRssiThresholdBreached(curRssi),
                        TAG + "#processClientRssiThresholdBreached");
            } else {
                mWifiThreadRunner.post(() -> handleRssiBreachRestartRssiMonitor(curRssi),
                        TAG + "#handleRssiBreachRestartRssiMonitor");
            }
        }
    }
    private final RssiEventHandler mRssiEventHandler = new RssiEventHandler();

    private void processClientRssiThresholdBreached(int curRssi) {
        int shortInterval = mWifiGlobals.getPollRssiShortIntervalMillis();
        logd("Client mode RSSI monitor threshold breach event. RSSI polling interval changed to "
                + shortInterval + " ms" + ", disable client mode RSSI monitor");
        mWifiGlobals.setPollRssiIntervalMillis(shortInterval);
        disableClientRssiMonitorAndUpdateThresholds(curRssi);
    }

    private void handleRssiBreachRestartRssiMonitor(byte curRssi) {
        if (curRssi == Byte.MAX_VALUE || curRssi == Byte.MIN_VALUE) {
            Log.wtf(getTag(), "Process RSSI thresholds: Invalid rssi " + curRssi);
            return;
        }
        for (int i = 1; i < mRssiRanges.length; i++) {
            if (curRssi < mRssiRanges[i]) {
                // Assume sorted values(ascending order) for rssi,
                // bounded by high(127) and low(-128) at extremeties
                byte maxRssi = mRssiRanges[i];
                byte minRssi = mRssiRanges[i - 1];
                // This value of hw has to be believed as this value is averaged and has breached
                // the rssi thresholds and raised event to host. This would be eggregious if this
                // value is invalid
                mWifiInfo.setRssi(curRssi);
                mUpdateCapabilityRunnable.run();
                int ret = startRssiMonitoringOffload(maxRssi, minRssi);
                logd("Re-program RSSI thresholds " + ": [" + minRssi + ", "
                        + maxRssi + "], curRssi=" + curRssi + " ret=" + ret);
                break;
            }
        }
    }

    private int startRssiMonitoringOffload(byte maxRssi, byte minRssi) {
        return mWifiNative.startRssiMonitoring(mInterfaceName, maxRssi, minRssi, mRssiEventHandler);
    }

    private int stopRssiMonitoringOffload() {
        return mWifiNative.stopRssiMonitoring(mInterfaceName);
    }

    /**
     * Reset the RSSI monitor
     */
    public void reset() {
        mEnableClientRssiMonitor = false;
        mAppThresholds = new int[] {};
        mRssiRanges = new byte[] {};
        if (!mIsPollRssiIntervalOverridden) {
            int shortInterval = mWifiGlobals.getPollRssiShortIntervalMillis();
            mWifiGlobals.setPollRssiIntervalMillis(shortInterval);
        }
        stopRssiMonitoringOffload();
    }

    /**
     * Enable/Disable verbose logging.
     * @param verbose true to enable and false to disable.
     */
    public void enableVerboseLogging(boolean verbose) {
        mVerboseLoggingEnabled = verbose;
    }

    /**
     * Update the RSSI polling interval based on the current device mobility state and RSSI.
     * If the device is stationary and RSSI is high, change to the long interval. Otherwise,
     * change to the short interval.
     * @param state the current device mobility state
     */
    public void updatePollRssiInterval(@DeviceMobilityState int state) {
        if (!mWifiGlobals.isAdjustPollRssiIntervalEnabled()
                || !mDeviceConfigFacade.isAdjustPollRssiIntervalEnabled()
                || mIsPollRssiIntervalOverridden) {
            return;
        }
        int curRssi = mWifiInfo.getRssi();
        int rssiMonitorThreshold = mWifiGlobals.getClientRssiMonitorThresholdDbm();
        int rssiMonitorHysteresisDb = mWifiGlobals.getClientRssiMonitorHysteresisDb();
        if (state == WifiManager.DEVICE_MOBILITY_STATE_STATIONARY && curRssi
                >= rssiMonitorThreshold + rssiMonitorHysteresisDb) {
            setLongPollRssiInterval();
        } else if (state != WifiManager.DEVICE_MOBILITY_STATE_STATIONARY || curRssi
                < rssiMonitorThreshold) {
            setShortPollRssiInterval();
        }
    }

    private void setLongPollRssiInterval() {
        int longInterval = mWifiGlobals.getPollRssiLongIntervalMillis();
        if (mWifiGlobals.getPollRssiIntervalMillis() == longInterval) {
            return;
        }
        logd("RSSI polling interval changed to " + longInterval + " ms"
                + ", enable client mode RSSI monitor");
        mWifiGlobals.setPollRssiIntervalMillis(longInterval);
        enableClientRssiMonitorAndUpdateThresholds(mWifiInfo.getRssi());
    }

    /**
     * Change the RSSI polling interval to the short interval and disable client mode RSSI monitor
     */
    public void setShortPollRssiInterval() {
        if (mIsPollRssiIntervalOverridden) {
            return;
        }
        int shortInterval = mWifiGlobals.getPollRssiShortIntervalMillis();
        if (mWifiGlobals.getPollRssiIntervalMillis() == shortInterval) {
            return;
        }
        logd("RSSI polling interval changed to " + shortInterval + " ms"
                + ", disable client mode RSSI monitor");
        mWifiGlobals.setPollRssiIntervalMillis(shortInterval);
        disableClientRssiMonitorAndUpdateThresholds(mWifiInfo.getRssi());
    }

    private void enableClientRssiMonitorAndUpdateThresholds(int curRssi) {
        mEnableClientRssiMonitor = true;
        updateRssiRangesAndStartMonitor(curRssi);
    }

    private void disableClientRssiMonitorAndUpdateThresholds(int curRssi) {
        mEnableClientRssiMonitor = false;
        updateRssiRangesAndStartMonitor(curRssi);
    }

    /**
     * Pass the updated App RSSI thresholds to RssiMonitor and start/restart RSSI monitoring if
     * the thresholds are valid after processing.
     */
    public void updateAppThresholdsAndStartMonitor(@NonNull int[] appThresholds) {
        logd("Received app signal strength thresholds: " + Arrays.toString(appThresholds));
        mAppThresholds = appThresholds;
        updateRssiRangesAndStartMonitor(mWifiInfo.getRssi());
    }

    private void updateRssiRangesAndStartMonitor(int curRssi) {
        // 0. If there are no thresholds, or if the thresholds are invalid,
        //    stop RSSI monitoring.
        // 1. Tell the hardware to start RSSI monitoring here, possibly adding MIN_VALUE and
        //    MAX_VALUE at the start/end of the thresholds array if necessary.
        // 2. Ensure that when the hardware event fires, we fetch the RSSI from the hardware
        //    event, call mWifiInfo.setRssi() with it, and call updateCapabilities(), and then
        //    re-arm the hardware event. This needs to be done on the state machine thread to
        //    avoid race conditions. The RSSI used to re-arm the event (and perhaps also the one
        //    sent in the NetworkCapabilities) must be the one received from the hardware event
        //    received, or we might skip callbacks.
        // 3. Ensure that when we disconnect, RSSI monitoring is stopped.
        int[] mergedThresholds = mergeAppFrameworkThresholds(mAppThresholds);
        if (mergedThresholds.length == 0) {
            mRssiRanges = new byte[] {};
            stopRssiMonitoringOffload();
            return;
        }
        int [] rssiVals = Arrays.copyOf(mergedThresholds, mergedThresholds.length + 2);
        rssiVals[rssiVals.length - 2] = Byte.MIN_VALUE;
        rssiVals[rssiVals.length - 1] = Byte.MAX_VALUE;
        Arrays.sort(rssiVals);
        byte[] rssiRange = new byte[rssiVals.length];
        for (int i = 0; i < rssiVals.length; i++) {
            int val = rssiVals[i];
            if (val <= Byte.MAX_VALUE && val >= Byte.MIN_VALUE) {
                rssiRange[i] = (byte) val;
            } else {
                Log.e(getTag(), "Illegal value " + val + " for RSSI thresholds: "
                        + Arrays.toString(rssiVals));
                stopRssiMonitoringOffload();
                return;
            }
        }
        mRssiRanges = rssiRange;
        logd("Updated RSSI thresholds=" + Arrays.toString(mRssiRanges));
        handleRssiBreachRestartRssiMonitor((byte) curRssi);
    }

    private int[] mergeAppFrameworkThresholds(@NonNull int[] appThresholds) {
        if (mEnableClientRssiMonitor) {
            int[] mergedThresholds = Arrays.copyOf(appThresholds,
                    appThresholds.length + 1);
            mergedThresholds[mergedThresholds.length - 1] = mWifiGlobals
                    .getClientRssiMonitorThresholdDbm();
            return mergedThresholds;
        } else {
            return appThresholds;
        }
    }

    /**
     * When link layer stats polling interval is overridden, set the fixed interval and disable
     * client mode RSSI monitoring if it is enabled. When the polling interval is set to automatic
     * handling, set the interval to the regular (short) interval, then the RSSI monitor will
     * adjust the interval automatically
     * @param newIntervalMs a non-negative integer, for the link layer stats polling interval
     *                      in milliseconds.
     *                      To set a fixed interval, use a positive value.
     *                      For automatic handling of the interval, use value 0
     */
    public void overridePollRssiInterval(int newIntervalMs) {
        if (mIsPollRssiIntervalOverridden && newIntervalMs == 0) {
            setAutoPollRssiInterval();
            return;
        }
        if (newIntervalMs > 0) {
            setFixedPollRssiInterval(newIntervalMs);
        }
    }

    private void setAutoPollRssiInterval() {
        mIsPollRssiIntervalOverridden = false;
        int regularInterval = mWifiGlobals.getPollRssiShortIntervalMillis();
        mWifiGlobals.setPollRssiIntervalMillis(regularInterval);
    }

    private void setFixedPollRssiInterval(int newIntervalMs) {
        mIsPollRssiIntervalOverridden = true;
        mWifiGlobals.setPollRssiIntervalMillis(newIntervalMs);
        if (mEnableClientRssiMonitor) {
            disableClientRssiMonitorAndUpdateThresholds(mWifiInfo.getRssi());
        }
    }
}
