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

import android.app.admin.DevicePolicyManager;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.PackageManager;
import android.net.NetworkCapabilities;
import android.net.TetheringManager;
import android.net.wifi.ScanResult;
import android.net.wifi.SoftApConfiguration;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiManager;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;

import androidx.annotation.VisibleForTesting;

import com.android.settings.R;
import com.android.settings.Utils;
import com.android.wifitrackerlib.WifiEntry;

import java.nio.charset.StandardCharsets;

/** A utility class for Wi-Fi functions. */
public class WifiUtils extends com.android.settingslib.wifi.WifiUtils {

    static final String TAG = "WifiUtils";

    private static final int SSID_ASCII_MIN_LENGTH = 1;
    private static final int SSID_ASCII_MAX_LENGTH = 32;

    private static final int PSK_PASSPHRASE_ASCII_MIN_LENGTH = 8;
    private static final int PSK_PASSPHRASE_ASCII_MAX_LENGTH = 63;

    private static Boolean sCanShowWifiHotspotCached;

    public static boolean isSSIDTooLong(String ssid) {
        if (TextUtils.isEmpty(ssid)) {
            return false;
        }
        return ssid.getBytes(StandardCharsets.UTF_8).length > SSID_ASCII_MAX_LENGTH;
    }

    public static boolean isSSIDTooShort(String ssid) {
        if (TextUtils.isEmpty(ssid)) {
            return true;
        }
        return ssid.length() < SSID_ASCII_MIN_LENGTH;
    }

    /**
     * Check if the hotspot password is valid.
     */
    public static boolean isHotspotPasswordValid(String password, int securityType) {
        final SoftApConfiguration.Builder configBuilder = new SoftApConfiguration.Builder();
        try {
            if (securityType == SoftApConfiguration.SECURITY_TYPE_WPA2_PSK
                    || securityType == SoftApConfiguration.SECURITY_TYPE_WPA3_SAE_TRANSITION) {
                if (password.length() < PSK_PASSPHRASE_ASCII_MIN_LENGTH
                        || password.length() > PSK_PASSPHRASE_ASCII_MAX_LENGTH) {
                    return false;
                }
            }
            configBuilder.setPassphrase(password, securityType);
        } catch (Exception e) {
            return false;
        }
        return true;
    }

    /**
     * This method is a stripped and negated version of WifiConfigStore.canModifyNetwork.
     *
     * @param context Context of caller
     * @param config  The WiFi config.
     * @return true if Settings cannot modify the config due to lockDown.
     */
    public static boolean isNetworkLockedDown(Context context, WifiConfiguration config) {
        if (context == null || config == null) {
            return false;
        }

        final DevicePolicyManager dpm =
                (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);
        final PackageManager pm = context.getPackageManager();
        final UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE);

        // Check if device has DPM capability. If it has and dpm is still null, then we
        // treat this case with suspicion and bail out.
        if (pm.hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN) && dpm == null) {
            return true;
        }

        boolean isConfigEligibleForLockdown = false;
        if (dpm != null) {
            final ComponentName deviceOwner = dpm.getDeviceOwnerComponentOnAnyUser();
            if (deviceOwner != null) {
                final int deviceOwnerUserId = dpm.getDeviceOwnerUserId();
                try {
                    final int deviceOwnerUid = pm.getPackageUidAsUser(deviceOwner.getPackageName(),
                            deviceOwnerUserId);
                    isConfigEligibleForLockdown = deviceOwnerUid == config.creatorUid;
                } catch (PackageManager.NameNotFoundException e) {
                    // don't care
                }
            } else if (dpm.isOrganizationOwnedDeviceWithManagedProfile()) {
                int profileOwnerUserId = Utils.getManagedProfileId(um, UserHandle.myUserId());
                final ComponentName profileOwner = dpm.getProfileOwnerAsUser(profileOwnerUserId);
                if (profileOwner != null) {
                    try {
                        final int profileOwnerUid = pm.getPackageUidAsUser(
                                profileOwner.getPackageName(), profileOwnerUserId);
                        isConfigEligibleForLockdown = profileOwnerUid == config.creatorUid;
                    } catch (PackageManager.NameNotFoundException e) {
                        // don't care
                    }
                }
            }
        }
        if (!isConfigEligibleForLockdown) {
            return false;
        }

        final ContentResolver resolver = context.getContentResolver();
        final boolean isLockdownFeatureEnabled = Settings.Global.getInt(resolver,
                Settings.Global.WIFI_DEVICE_OWNER_CONFIGS_LOCKDOWN, 0) != 0;
        return isLockdownFeatureEnabled;
    }

    /** Returns true if the provided NetworkCapabilities indicate a captive portal network. */
    public static boolean canSignIntoNetwork(NetworkCapabilities capabilities) {
        return (capabilities != null
                && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL));
    }

    /**
     * Provides a simple way to generate a new {@link WifiConfiguration} obj from
     * {@link ScanResult} or {@link WifiEntry}. Either {@code wifiEntry} or {@code scanResult
     * } input should be not null for retrieving information, otherwise will throw
     * IllegalArgumentException.
     * This method prefers to take {@link WifiEntry} input in priority. Therefore this method
     * will take {@link WifiEntry} input as preferred data extraction source when you input
     * both {@link WifiEntry} and {@link ScanResult}, and ignore {@link ScanResult} input.
     *
     * Duplicated and simplified method from {@link WifiConfigController#getConfig()}.
     * TODO(b/120827021): Should be removed if the there is have a common one in shared place (e.g.
     * SettingsLib).
     *
     * @param wifiEntry Input data for retrieving WifiConfiguration.
     * @param scanResult  Input data for retrieving WifiConfiguration.
     * @return WifiConfiguration obj based on input.
     */
    public static WifiConfiguration getWifiConfig(WifiEntry wifiEntry, ScanResult scanResult) {
        if (wifiEntry == null && scanResult == null) {
            throw new IllegalArgumentException(
                    "At least one of WifiEntry and ScanResult input is required.");
        }

        final WifiConfiguration config = new WifiConfiguration();
        final int security;

        if (wifiEntry == null) {
            config.SSID = "\"" + scanResult.SSID + "\"";
            security = getWifiEntrySecurity(scanResult);
        } else {
            if (wifiEntry.getWifiConfiguration() == null) {
                config.SSID = "\"" + wifiEntry.getSsid() + "\"";
            } else {
                config.networkId = wifiEntry.getWifiConfiguration().networkId;
                config.hiddenSSID = wifiEntry.getWifiConfiguration().hiddenSSID;
            }
            security = wifiEntry.getSecurity();
        }

        switch (security) {
            case WifiEntry.SECURITY_NONE:
                config.setSecurityParams(WifiConfiguration.SECURITY_TYPE_OPEN);
                break;

            case WifiEntry.SECURITY_WEP:
                config.setSecurityParams(WifiConfiguration.SECURITY_TYPE_WEP);
                break;

            case WifiEntry.SECURITY_PSK:
                config.setSecurityParams(WifiConfiguration.SECURITY_TYPE_PSK);
                break;

            case WifiEntry.SECURITY_EAP_SUITE_B:
                config.setSecurityParams(WifiConfiguration.SECURITY_TYPE_EAP_SUITE_B);
                break;

            case WifiEntry.SECURITY_EAP:
                config.setSecurityParams(WifiConfiguration.SECURITY_TYPE_EAP);
                break;

            case WifiEntry.SECURITY_SAE:
                config.setSecurityParams(WifiConfiguration.SECURITY_TYPE_SAE);
                break;

            case WifiEntry.SECURITY_OWE:
                config.setSecurityParams(WifiConfiguration.SECURITY_TYPE_OWE);
                break;

            default:
                break;
        }
        return config;
    }

    /**
     * Gets security value from ScanResult.
     *
     * @param result ScanResult
     * @return Related security value based on {@link WifiEntry}.
     */
    public static int getWifiEntrySecurity(ScanResult result) {
        if (result.capabilities.contains("WEP")) {
            return WifiEntry.SECURITY_WEP;
        } else if (result.capabilities.contains("SAE")) {
            return WifiEntry.SECURITY_SAE;
        } else if (result.capabilities.contains("PSK")) {
            return WifiEntry.SECURITY_PSK;
        } else if (result.capabilities.contains("EAP_SUITE_B_192")) {
            return WifiEntry.SECURITY_EAP_SUITE_B;
        } else if (result.capabilities.contains("EAP")) {
            return WifiEntry.SECURITY_EAP;
        } else if (result.capabilities.contains("OWE")) {
            return WifiEntry.SECURITY_OWE;
        }

        return WifiEntry.SECURITY_NONE;
    }

    /**
     * Check if Wi-Fi hotspot settings can be displayed.
     *
     * @param context Context of caller
     * @return true if Wi-Fi hotspot settings can be displayed
     */
    public static boolean checkShowWifiHotspot(Context context) {
        if (context == null) return false;

        boolean showWifiHotspotSettings =
                context.getResources().getBoolean(R.bool.config_show_wifi_hotspot_settings);
        if (!showWifiHotspotSettings) {
            Log.w(TAG, "config_show_wifi_hotspot_settings:false");
            return false;
        }

        WifiManager wifiManager = context.getSystemService(WifiManager.class);
        if (wifiManager == null) {
            Log.e(TAG, "WifiManager is null");
            return false;
        }

        TetheringManager tetheringManager = context.getSystemService(TetheringManager.class);
        if (tetheringManager == null) {
            Log.e(TAG, "TetheringManager is null");
            return false;
        }
        String[] wifiRegexs = tetheringManager.getTetherableWifiRegexs();
        if (wifiRegexs == null || wifiRegexs.length == 0) {
            Log.w(TAG, "TetherableWifiRegexs is empty");
            return false;
        }
        return true;
    }

    /**
     * Return the cached result to see if Wi-Fi hotspot settings can be displayed.
     *
     * @param context Context of caller
     * @return true if Wi-Fi hotspot settings can be displayed
     */
    public static boolean canShowWifiHotspot(Context context) {
        if (sCanShowWifiHotspotCached == null) {
            sCanShowWifiHotspotCached = checkShowWifiHotspot(context);
        }
        return sCanShowWifiHotspotCached;
    }

    /**
     * Sets the sCanShowWifiHotspotCached for testing purposes.
     *
     * @param cached Cached value for #canShowWifiHotspot()
     */
    @VisibleForTesting
    public static void setCanShowWifiHotspotCached(Boolean cached) {
        sCanShowWifiHotspotCached = cached;
    }
}
