/*
 * Copyright (C) 2019 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 static android.net.wifi.WifiManager.WIFI_FEATURE_MBO;
import static android.net.wifi.WifiManager.WIFI_FEATURE_OCE;

import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.util.Log;

import com.android.server.wifi.SupplicantStaIfaceHal.MboAssocDisallowedReasonCode;

/**
 * MboOceController is responsible for controlling MBO and OCE operations.
 */
public class MboOceController {
    private static final String TAG = "MboOceController";

    /** State of MBO/OCE module. */
    private boolean mEnabled = false;
    private boolean mIsMboSupported = false;
    private boolean mIsOceSupported = false;
    private boolean mVerboseLoggingEnabled = false;

    private final TelephonyManager mTelephonyManager;
    private final ActiveModeWarden mActiveModeWarden;
    private final WifiThreadRunner mWifiThreadRunner;

    /**
     * Create new instance of MboOceController.
     */
    public MboOceController(TelephonyManager telephonyManager, ActiveModeWarden activeModeWarden,
            WifiThreadRunner wifiThreadRunner) {
        mTelephonyManager = telephonyManager;
        mActiveModeWarden = activeModeWarden;
        mWifiThreadRunner = wifiThreadRunner;
    }

    /**
     * Enable MBO and OCE functionality.
     */
    public void enable() {
        ClientModeManager clientModeManager =
                mActiveModeWarden.getPrimaryClientModeManagerNullable();
        if (clientModeManager == null) {
            return;
        }
        long supportedFeatures = clientModeManager.getSupportedFeatures();
        mIsMboSupported = (supportedFeatures & WIFI_FEATURE_MBO) != 0;
        mIsOceSupported = (supportedFeatures & WIFI_FEATURE_OCE) != 0;
        mEnabled = true;
        if (mVerboseLoggingEnabled) {
            Log.d(TAG, "Enable MBO-OCE MBO support: " + mIsMboSupported
                    + " OCE support: " + mIsOceSupported);
        }
        if (mIsMboSupported) {
            // Register for data connection state change events (Cellular).
            mTelephonyManager.listen(mDataConnectionStateListener,
                    PhoneStateListener.LISTEN_DATA_CONNECTION_STATE);
        }
    }

    /**
     * Disable MBO and OCE functionality.
     */
    public void disable() {
        if (mVerboseLoggingEnabled) {
            Log.d(TAG, "Disable MBO-OCE");
        }
        if (mIsMboSupported) {
            // Un-register for data connection state change events (Cellular).
            mTelephonyManager.listen(mDataConnectionStateListener, PhoneStateListener.LISTEN_NONE);
        }
        mEnabled = false;
    }

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

    /**
     * Listen for changes to the data connection state (Cellular).
     */
    private PhoneStateListener mDataConnectionStateListener = new PhoneStateListener(){
        public void onDataConnectionStateChanged(int state, int networkType) {
            mWifiThreadRunner.post(
                    () -> {
                        boolean dataAvailable;
                        ClientModeManager clientModeManager =
                                mActiveModeWarden.getPrimaryClientModeManagerNullable();
                        if (clientModeManager == null) {
                            return;
                        }
                        if (!mEnabled) {
                            Log.e(TAG, "onDataConnectionStateChanged called when MBO is "
                                    + "disabled!!");
                            return;
                        }
                        if (state == TelephonyManager.DATA_CONNECTED) {
                            dataAvailable = true;
                        } else if (state == TelephonyManager.DATA_DISCONNECTED) {
                            dataAvailable = false;
                        } else {
                            Log.e(TAG, "onDataConnectionStateChanged unexpected State: " + state);
                            return;
                        }
                        if (mVerboseLoggingEnabled) {
                            Log.d(TAG, "Cell Data: " + dataAvailable);
                        }
                        clientModeManager.setMboCellularDataStatus(dataAvailable);
                    }, TAG + "#onDataConnectionStateChanged"
            );
        }
    };

    /**
     * BtmFrameData carries the data retried from received BTM
     * request frame handled in supplicant.
     */
    public static class BtmFrameData {
        public @MboOceConstants.BtmResponseStatus int mStatus =
                MboOceConstants.BTM_RESPONSE_STATUS_INVALID;
        public int mBssTmDataFlagsMask = 0;
        public long mBlockListDurationMs = 0;
        public @MboOceConstants.MboTransitionReason int mTransitionReason =
                MboOceConstants.MBO_TRANSITION_REASON_INVALID;
        public @MboOceConstants.MboCellularDataConnectionPreference int mCellPreference =
                MboOceConstants.MBO_CELLULAR_DATA_CONNECTION_INVALID;

        @Override
        public String toString() {
            return new StringBuilder("BtmFrameData status=").append(mStatus).append(
                    ", flags=").append(mBssTmDataFlagsMask).append(
                    ", assocRetryDelay=").append(mBlockListDurationMs).append(
                    ", transitionReason=").append(mTransitionReason).append(
                    ", cellPref=").append(mCellPreference).toString();
        }
    }

    /**
     * OceRssiBasedAssocRejectAttr is extracted from (Re-)Association response frame from an OCE AP
     * to indicate that the AP has rejected the (Re-)Association request on the basis of
     * insufficient RSSI.
     * Refer OCE spec v1.0 section 4.2.2 Table 7.
     */
    public static class OceRssiBasedAssocRejectAttr {
        /*
         * Delta RSSI - The difference in dB between the minimum RSSI at which
         * the AP would accept a (Re-)Association request from the STA before
         * Retry Delay expires and the AP's measurement of the RSSI at which the
         * (Re-)Association request was received.
         */
        public int mDeltaRssi;
        /*
         * Retry Delay - The time period in seconds for which the AP will not
         * accept any subsequent (Re-)Association requests from the STA, unless
         * the received RSSI has improved by Delta RSSI.
         */
        public int mRetryDelayS;

        public OceRssiBasedAssocRejectAttr(int deltaRssi, int retryDelayS) {
            this.mDeltaRssi = deltaRssi;
            this.mRetryDelayS = retryDelayS;
        }

        @Override
        public String toString() {
            return new StringBuilder("OceRssiBasedAssocRejectAttr Delta Rssi=")
                    .append(mDeltaRssi).append(
                    ", Retry Delay=").append(mRetryDelayS).toString();
        }
    }

    /**
     * MboAssocDisallowedAttr is extracted from (Re-)Association response frame from the MBO AP
     * to indicate that the AP is not accepting new associations.
     * Refer MBO spec v1.2 section 4.2.4 Table 13 for the details of reason code.
     */
    public static class MboAssocDisallowedAttr {
        /*
         * Reason Code - The reason why the AP is not accepting new
         * associations.
         */
        public @MboOceConstants.MboAssocDisallowedReasonCode int mReasonCode;

        public MboAssocDisallowedAttr(int reasonCode) {
            mReasonCode = halToFrameworkMboAssocRDisallowedReasonCode(reasonCode);
        }

        @Override
        public String toString() {
            return new StringBuilder("MboAssocDisallowedAttr Reason code=")
                    .append(mReasonCode).toString();
        }

        private @MboOceConstants.MboAssocDisallowedReasonCode int
                halToFrameworkMboAssocRDisallowedReasonCode(int reasonCode) {
            switch (reasonCode) {
                case MboAssocDisallowedReasonCode.RESERVED:
                    return MboOceConstants.MBO_ASSOC_DISALLOWED_REASON_RESERVED_0;
                case MboAssocDisallowedReasonCode.UNSPECIFIED:
                    return MboOceConstants.MBO_ASSOC_DISALLOWED_REASON_UNSPECIFIED;
                case MboAssocDisallowedReasonCode.MAX_NUM_STA_ASSOCIATED:
                    return MboOceConstants.MBO_ASSOC_DISALLOWED_REASON_MAX_NUM_STA_ASSOCIATED;
                case MboAssocDisallowedReasonCode.AIR_INTERFACE_OVERLOADED:
                    return MboOceConstants.MBO_ASSOC_DISALLOWED_REASON_AIR_INTERFACE_OVERLOADED;
                case MboAssocDisallowedReasonCode.AUTH_SERVER_OVERLOADED:
                    return MboOceConstants.MBO_ASSOC_DISALLOWED_REASON_AUTH_SERVER_OVERLOADED;
                case MboAssocDisallowedReasonCode.INSUFFICIENT_RSSI:
                    return MboOceConstants.MBO_ASSOC_DISALLOWED_REASON_INSUFFICIENT_RSSI;
                default:
                    return MboOceConstants.MBO_ASSOC_DISALLOWED_REASON_RESERVED;
            }
        }
    }
}
