/*
 * Copyright (C) 2017 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.ScanResult;
import android.net.wifi.WifiConfiguration;

import com.android.server.wifi.util.ScanResultUtil;

import java.util.Objects;

/**
 * Class to store the info needed to match a scan result to the provided network configuration.
 */
public class ScanResultMatchInfo {
    /**
     * SSID of the network.
     */
    public String networkSsid;
    /**
     * Security Type of the network.
     */
    public @WifiConfiguration.SecurityType int networkType;
    /**
     * Special flag for PSK-SAE in transition mode
     */
    public boolean pskSaeInTransitionMode;
    /**
     * Special flag for OWE in transition mode
     */
    public boolean oweInTransitionMode;

    /**
     * True if created from a scan result
     */
    private boolean mFromScanResult = false;
    /**
     * Fetch network type from network configuration.
     */
    private static @WifiConfiguration.SecurityType int getNetworkType(WifiConfiguration config) {
        if (WifiConfigurationUtil.isConfigForSaeNetwork(config)) {
            return WifiConfiguration.SECURITY_TYPE_SAE;
        } else if (WifiConfigurationUtil.isConfigForPskNetwork(config)) {
            return WifiConfiguration.SECURITY_TYPE_PSK;
        } else if (WifiConfigurationUtil.isConfigForWapiPskNetwork(config)) {
            return WifiConfiguration.SECURITY_TYPE_WAPI_PSK;
        } else if (WifiConfigurationUtil.isConfigForWapiCertNetwork(config)) {
            return WifiConfiguration.SECURITY_TYPE_WAPI_CERT;
        } else if (WifiConfigurationUtil.isConfigForEapNetwork(config)) {
            return WifiConfiguration.SECURITY_TYPE_EAP;
        } else if (WifiConfigurationUtil.isConfigForEapSuiteBNetwork(config)) {
            return WifiConfiguration.SECURITY_TYPE_EAP_SUITE_B;
        } else if (WifiConfigurationUtil.isConfigForWepNetwork(config)) {
            return WifiConfiguration.SECURITY_TYPE_WEP;
        } else if (WifiConfigurationUtil.isConfigForOweNetwork(config)) {
            return WifiConfiguration.SECURITY_TYPE_OWE;
        } else if (WifiConfigurationUtil.isConfigForOpenNetwork(config)) {
            return WifiConfiguration.SECURITY_TYPE_OPEN;
        }
        throw new IllegalArgumentException("Invalid WifiConfiguration: " + config);
    }

    /**
     * Get the ScanResultMatchInfo for the given WifiConfiguration
     */
    public static ScanResultMatchInfo fromWifiConfiguration(WifiConfiguration config) {
        ScanResultMatchInfo info = new ScanResultMatchInfo();
        info.networkSsid = config.SSID;
        info.networkType = getNetworkType(config);
        return info;
    }

    /**
     * Fetch network type from scan result.
     */
    private static @WifiConfiguration.SecurityType int getNetworkType(ScanResult scanResult) {
        if (ScanResultUtil.isScanResultForSaeNetwork(scanResult)) {
            return WifiConfiguration.SECURITY_TYPE_SAE;
        } else if (ScanResultUtil.isScanResultForWapiPskNetwork(scanResult)) {
            return WifiConfiguration.SECURITY_TYPE_WAPI_PSK;
        } else if (ScanResultUtil.isScanResultForWapiCertNetwork(scanResult)) {
            return WifiConfiguration.SECURITY_TYPE_WAPI_CERT;
        } else if (ScanResultUtil.isScanResultForPskNetwork(scanResult)) {
            return WifiConfiguration.SECURITY_TYPE_PSK;
        } else if (ScanResultUtil.isScanResultForEapSuiteBNetwork(scanResult)) {
            return WifiConfiguration.SECURITY_TYPE_EAP_SUITE_B;
        } else if (ScanResultUtil.isScanResultForEapNetwork(scanResult)) {
            return WifiConfiguration.SECURITY_TYPE_EAP;
        } else if (ScanResultUtil.isScanResultForWepNetwork(scanResult)) {
            return WifiConfiguration.SECURITY_TYPE_WEP;
        } else if (ScanResultUtil.isScanResultForOweNetwork(scanResult)) {
            return WifiConfiguration.SECURITY_TYPE_OWE;
        } else if (ScanResultUtil.isScanResultForOpenNetwork(scanResult)) {
            return WifiConfiguration.SECURITY_TYPE_OPEN;
        } else {
            throw new IllegalArgumentException("Invalid ScanResult: " + scanResult);
        }
    }

    /**
     * Get the ScanResultMatchInfo for the given ScanResult
     */
    public static ScanResultMatchInfo fromScanResult(ScanResult scanResult) {
        ScanResultMatchInfo info = new ScanResultMatchInfo();
        // Scan result ssid's are not quoted, hence add quotes.
        // TODO: This matching algo works only if the scan result contains a string SSID.
        // However, according to our public documentation ths {@link WifiConfiguration#SSID} can
        // either have a hex string or quoted ASCII string SSID.
        info.networkSsid = ScanResultUtil.createQuotedSSID(scanResult.SSID);
        info.networkType = getNetworkType(scanResult);
        info.oweInTransitionMode = false;
        info.pskSaeInTransitionMode = false;
        info.mFromScanResult = true;
        if (info.networkType == WifiConfiguration.SECURITY_TYPE_SAE) {
            // Note that scan result util will always choose the highest security protocol.
            info.pskSaeInTransitionMode =
                    ScanResultUtil.isScanResultForPskSaeTransitionNetwork(scanResult);
        } else  if (info.networkType == WifiConfiguration.SECURITY_TYPE_OWE) {
            // Note that scan result util will always choose OWE.
            info.oweInTransitionMode =
                    ScanResultUtil.isScanResultForOweTransitionNetwork(scanResult);
        }
        return info;
    }

    /**
     * Checks for equality of network type.
     */
    public boolean networkTypeEquals(@NonNull ScanResultMatchInfo other,
            boolean saeAutoUpgradeEnabled) {
        boolean networkTypeEquals;
        // Detect <SSID, PSK+SAE> scan result and say it is equal to <SSID, PSK> configuration
        if (other.pskSaeInTransitionMode && networkType == WifiConfiguration.SECURITY_TYPE_PSK
                || (pskSaeInTransitionMode
                && other.networkType == WifiConfiguration.SECURITY_TYPE_PSK)) {
            networkTypeEquals = true;
        } else if ((networkType == WifiConfiguration.SECURITY_TYPE_OPEN
                && other.oweInTransitionMode) || (oweInTransitionMode
                && other.networkType == WifiConfiguration.SECURITY_TYPE_OPEN)) {
            // Special case we treat Enhanced Open and Open as equals. This is done to support the
            // case where a saved network is Open but we found an OWE in transition network.
            networkTypeEquals = true;
        } else if ((saeAutoUpgradeEnabled)
                && ((mFromScanResult && networkType == WifiConfiguration.SECURITY_TYPE_SAE
                && other.networkType == WifiConfiguration.SECURITY_TYPE_PSK)
                || (other.mFromScanResult
                && other.networkType == WifiConfiguration.SECURITY_TYPE_SAE
                && networkType == WifiConfiguration.SECURITY_TYPE_PSK))) {
            // Allow upgrading WPA2 PSK connections to WPA3 SAE AP
            networkTypeEquals = true;
        } else {
            networkTypeEquals = networkType == other.networkType;
        }
        return networkTypeEquals;
    }

    @Override
    public boolean equals(Object otherObj) {
        return matchForNetworkSelection(otherObj, false);
    }

    /**
     * Match two ScanResultMatchInfo objects while considering configuration in overlays
     *
     * @param otherObj Other object to compare against
     * @param saeAutoUpgradeEnabled A boolean that indicates if WPA3 auto upgrade feature is enabled
     * @return true if objects are equal for network selection purposes, false otherwise
     */
    public boolean matchForNetworkSelection(Object otherObj, boolean saeAutoUpgradeEnabled) {
        if (this == otherObj) {
            return true;
        } else if (!(otherObj instanceof ScanResultMatchInfo)) {
            return false;
        }
        ScanResultMatchInfo other = (ScanResultMatchInfo) otherObj;
        if (!Objects.equals(networkSsid, other.networkSsid)) {
            return false;
        }
        return networkTypeEquals(other, saeAutoUpgradeEnabled);
    }

    @Override
    public int hashCode() {
        return Objects.hash(networkSsid);
    }

    @Override
    public String toString() {
        return "ScanResultMatchInfo: SSID: " + networkSsid + ", type: " + networkType
                + ", WPA3 in transition mode: " + pskSaeInTransitionMode
                + ", OWE in transition mode: " + oweInTransitionMode + ", from scan result: "
                + mFromScanResult;
    }
}
