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

import static android.bluetooth.BluetoothDevice.METADATA_MODEL_NAME;

import android.app.settings.SettingsEnums;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.os.SystemProperties;
import android.sysprop.BluetoothProperties;
import android.text.TextUtils;
import android.util.Log;

import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceScreen;
import androidx.preference.SwitchPreferenceCompat;
import androidx.preference.TwoStatePreference;

import com.android.settings.R;
import com.android.settings.flags.Flags;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.bluetooth.A2dpProfile;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.HeadsetProfile;
import com.android.settingslib.bluetooth.LeAudioProfile;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.bluetooth.LocalBluetoothProfile;
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
import com.android.settingslib.bluetooth.MapProfile;
import com.android.settingslib.bluetooth.PanProfile;
import com.android.settingslib.bluetooth.PbapServerProfile;
import com.android.settingslib.core.lifecycle.Lifecycle;
import com.android.settingslib.utils.ThreadUtils;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;

/**
 * This class adds switches for toggling the individual profiles that a Bluetooth device
 * supports, such as "Phone audio", "Media audio", "Contact sharing", etc.
 */
public class BluetoothDetailsProfilesController extends BluetoothDetailsController
        implements Preference.OnPreferenceClickListener,
        LocalBluetoothProfileManager.ServiceListener {
    public static final String HIGH_QUALITY_AUDIO_PREF_TAG = "A2dpProfileHighQualityAudio";

    private static final String TAG = "BtDetailsProfilesCtrl";

    private static final String KEY_PROFILES_GROUP = "bluetooth_profiles";
    private static final String KEY_BOTTOM_PREFERENCE = "bottom_preference";
    private static final int ORDINAL = 99;

    private static final String ENABLE_DUAL_MODE_AUDIO =
            "persist.bluetooth.enable_dual_mode_audio";
    private static final String LE_AUDIO_CONNECTION_BY_DEFAULT_PROPERTY =
            "ro.bluetooth.leaudio.le_audio_connection_by_default";
    private static final boolean LE_AUDIO_TOGGLE_VISIBLE_DEFAULT_VALUE = true;
    private static final String LE_AUDIO_TOGGLE_VISIBLE_PROPERTY =
            "persist.bluetooth.leaudio.toggle_visible";
    private static final String BYPASS_LE_AUDIO_ALLOWLIST_PROPERTY =
            "persist.bluetooth.leaudio.bypass_allow_list";
    private static final String LE_AUDIO_TOGGLE_VISIBLE_FOR_ASHA_PROPERTY =
            "bluetooth.leaudio.toggle_visible_for_asha";

    private Set<String> mInvisibleProfiles = Collections.emptySet();
    private final AtomicReference<Set<String>> mAdditionalInvisibleProfiles =
            new AtomicReference<>();

    private LocalBluetoothManager mManager;
    private LocalBluetoothProfileManager mProfileManager;
    private CachedBluetoothDevice mCachedDevice;
    private Set<CachedBluetoothDevice> mCachedDeviceGroup;
    private Map<String, List<CachedBluetoothDevice>> mProfileDeviceMap =
            new HashMap<String, List<CachedBluetoothDevice>>();
    private boolean mIsLeAudioToggleEnabled = false;
    private boolean mIsLeAudioOnlyDevice = false;
    private boolean mHasExtraSpace;

    @VisibleForTesting
    PreferenceCategory mProfilesContainer;

    public BluetoothDetailsProfilesController(
            Context context,
            PreferenceFragmentCompat fragment,
            LocalBluetoothManager manager,
            CachedBluetoothDevice device,
            Lifecycle lifecycle) {
        super(context, fragment, device, lifecycle);
        mManager = manager;
        mProfileManager = mManager.getProfileManager();
        mCachedDevice = device;
        mCachedDeviceGroup = Utils.findAllCachedBluetoothDevicesByGroupId(mManager, mCachedDevice);
    }

    /** Sets the profiles to be hidden. */
    public void setInvisibleProfiles(List<String> invisibleProfiles) {
        if (invisibleProfiles != null) {
            mInvisibleProfiles = Set.copyOf(invisibleProfiles);
        }
    }

    /** Sets whether it should show an extra padding on top of the preference. */
    public void setHasExtraSpace(boolean hasExtraSpace) {
        if (hasExtraSpace) {
            mProfilesContainer.setLayoutResource(R.layout.preference_bluetooth_profile_category);
        } else {
            mProfilesContainer.setLayoutResource(R.layout.preference_category_bluetooth_no_padding);
        }
    }

    @Override
    protected void init(PreferenceScreen screen) {
        mProfilesContainer = (PreferenceCategory) screen.findPreference(getPreferenceKey());
        mProfilesContainer.setLayoutResource(R.layout.preference_bluetooth_profile_category);
        // Call refresh here even though it will get called later in onResume, to avoid the
        // list of switches appearing to "pop" into the page.
        refresh();
    }

    /**
     * Creates a switch preference for the particular profile.
     *
     * @param context The context to use when creating the TwoStatePreference
     * @param profile The profile for which the preference controls.
     * @return A preference that allows the user to choose whether this profile
     * will be connected to.
     */
    private TwoStatePreference createProfilePreference(Context context,
            LocalBluetoothProfile profile) {
        TwoStatePreference pref = new SwitchPreferenceCompat(context);
        pref.setKey(profile.toString());
        pref.setTitle(profile.getNameResource(mCachedDevice.getDevice()));
        pref.setOnPreferenceClickListener(this);
        pref.setOrder(profile.getOrdinal());

        boolean isLeEnabledByDefault =
                SystemProperties.getBoolean(LE_AUDIO_CONNECTION_BY_DEFAULT_PROPERTY, true);

        if (profile instanceof LeAudioProfile && (!isLeEnabledByDefault || !isModelNameInAllowList(
                BluetoothUtils.getStringMetaData(mCachedDevice.getDevice(),
                        METADATA_MODEL_NAME)))) {
            pref.setSummary(R.string.device_details_leaudio_toggle_summary);
        }
        return pref;
    }

    /**
     * Checks if the device model name is in the LE audio allow list based on its model name.
     *
     * @param modelName The model name of the device to be checked.
     * @return true if the device is in the allow list, false otherwise.
     */
    @VisibleForTesting
    boolean isModelNameInAllowList(String modelName) {
        if (modelName == null || modelName.isEmpty()) {
            return false;
        }
        return BluetoothProperties.le_audio_allow_list().contains(modelName);
    }

    /**
     * Refreshes the state for an existing TwoStatePreference for a profile.
     */
    private void refreshProfilePreference(TwoStatePreference profilePref,
            LocalBluetoothProfile profile) {
        BluetoothDevice device = mCachedDevice.getDevice();
        boolean isLeAudioEnabled = isLeAudioEnabled();
        if (profile instanceof A2dpProfile || profile instanceof HeadsetProfile
                || profile instanceof LeAudioProfile) {
            List<CachedBluetoothDevice> deviceList = mProfileDeviceMap.get(
                    profile.toString());
            boolean isBusy = deviceList != null
                    && deviceList.stream().anyMatch(item -> item.isBusy());
            profilePref.setEnabled(!isBusy);
        } else {
            profilePref.setEnabled(!mCachedDevice.isBusy());
        }

        if (profile instanceof LeAudioProfile) {
            boolean showLeAudioToggle = mIsLeAudioToggleEnabled;
            if (Flags.hideLeAudioToggleForLeAudioOnlyDevice() && mIsLeAudioOnlyDevice) {
                showLeAudioToggle = false;
                Log.d(
                        TAG,
                        "Hide LeAudio toggle for LeAudio-only Device: "
                                + mCachedDevice.getDevice().getAnonymizedAddress());
            }
            profilePref.setVisible(showLeAudioToggle);
        }

        if (profile instanceof MapProfile) {
            profilePref.setChecked(device.getMessageAccessPermission()
                    == BluetoothDevice.ACCESS_ALLOWED);
        } else if (profile instanceof PbapServerProfile) {
            profilePref.setChecked(device.getPhonebookAccessPermission()
                    == BluetoothDevice.ACCESS_ALLOWED);
            profilePref.setSummary(profile.getSummaryResourceForDevice(mCachedDevice.getDevice()));
        } else if (profile instanceof PanProfile) {
            profilePref.setChecked(profile.getConnectionStatus(device) ==
                    BluetoothProfile.STATE_CONNECTED);
        } else {
            profilePref.setChecked(profile.isEnabled(device));
        }

        if (profile instanceof A2dpProfile) {
            A2dpProfile a2dp = (A2dpProfile) profile;
            TwoStatePreference highQualityPref =
                    mProfilesContainer.findPreference(HIGH_QUALITY_AUDIO_PREF_TAG);
            if (highQualityPref != null) {
                if (a2dp.isEnabled(device) && a2dp.supportsHighQualityAudio(device)) {
                    highQualityPref.setVisible(true);
                    highQualityPref.setTitle(a2dp.getHighQualityAudioOptionLabel(device));
                    highQualityPref.setChecked(a2dp.isHighQualityAudioEnabled(device));
                    highQualityPref.setEnabled(!mCachedDevice.isBusy());
                } else {
                    highQualityPref.setVisible(false);
                }
            }
        }
    }

    private boolean isLeAudioEnabled(){
        LocalBluetoothProfile leAudio = mProfileManager.getLeAudioProfile();
        if (leAudio != null) {
            List<CachedBluetoothDevice> leAudioDeviceList = mProfileDeviceMap.get(
                    leAudio.toString());
            if (leAudioDeviceList != null
                    && leAudioDeviceList.stream()
                    .anyMatch(item -> leAudio.isEnabled(item.getDevice()))) {
                return true;
            }
        }
        return false;
    }

    /**
     * Helper method to enable a profile for a device.
     */
    private void enableProfile(LocalBluetoothProfile profile) {
        final BluetoothDevice bluetoothDevice = mCachedDevice.getDevice();
        if (profile instanceof PbapServerProfile) {
            bluetoothDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
            // We don't need to do the additional steps below for this profile.
            return;
        }
        if (profile instanceof MapProfile) {
            bluetoothDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
        }

        if (profile instanceof LeAudioProfile) {
            enableLeAudioProfile(profile);
            return;
        }

        profile.setEnabled(bluetoothDevice, true);
    }

    /**
     * Helper method to disable a profile for a device
     */
    private void disableProfile(LocalBluetoothProfile profile) {
        if (profile instanceof LeAudioProfile) {
            disableLeAudioProfile(profile);
            return;
        }

        final BluetoothDevice bluetoothDevice = mCachedDevice.getDevice();
        profile.setEnabled(bluetoothDevice, false);

        if (profile instanceof MapProfile) {
            bluetoothDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED);
        } else if (profile instanceof PbapServerProfile) {
            bluetoothDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
        }
    }

    /**
     * When the pref for a bluetooth profile is clicked on, we want to toggle the enabled/disabled
     * state for that profile.
     */
    @Override
    public boolean onPreferenceClick(Preference preference) {
        LocalBluetoothProfile profile = mProfileManager.getProfileByName(preference.getKey());
        if (profile == null) {
            // It might be the PbapServerProfile, which is not stored by name.
            PbapServerProfile psp = mManager.getProfileManager().getPbapProfile();
            if (TextUtils.equals(preference.getKey(), psp.toString())) {
                profile = psp;
            } else {
                return false;
            }
        }
        TwoStatePreference profilePref = (TwoStatePreference) preference;
        if (profilePref.isChecked()) {
            enableProfile(profile);
        } else {
            disableProfile(profile);
        }
        refreshProfilePreference(profilePref, profile);
        return true;
    }

    /**
     * Helper to get the list of connectable and special profiles.
     */
    private List<LocalBluetoothProfile> getProfiles() {
        List<LocalBluetoothProfile> result = new ArrayList<>();
        mProfileDeviceMap.clear();
        if (mCachedDeviceGroup == null || mCachedDeviceGroup.isEmpty()) {
            return result;
        }
        for (CachedBluetoothDevice cachedItem : mCachedDeviceGroup) {
            List<LocalBluetoothProfile> tmpResult = cachedItem.getUiAccessibleProfiles();
            for (LocalBluetoothProfile profile : tmpResult) {
                if (mProfileDeviceMap.containsKey(profile.toString())) {
                    mProfileDeviceMap.get(profile.toString()).add(cachedItem);
                } else {
                    List<CachedBluetoothDevice> tmpCachedDeviceList = new ArrayList<>();
                    tmpCachedDeviceList.add(cachedItem);
                    mProfileDeviceMap.put(profile.toString(), tmpCachedDeviceList);
                    result.add(profile);
                }
            }
        }

        final BluetoothDevice device = mCachedDevice.getDevice();
        final int pbapPermission = device.getPhonebookAccessPermission();
        // Only provide PBAP cabability if the client device has requested PBAP.
        if (pbapPermission != BluetoothDevice.ACCESS_UNKNOWN) {
            final PbapServerProfile psp = mManager.getProfileManager().getPbapProfile();
            if (psp != null) {
                result.add(psp);
            }
        }

        final MapProfile mapProfile = mManager.getProfileManager().getMapProfile();
        final int mapPermission = device.getMessageAccessPermission();
        if (mapPermission != BluetoothDevice.ACCESS_UNKNOWN && mapProfile != null) {
            result.add(mapProfile);
        }

        // Removes phone calls & media audio toggles for dual mode devices
        boolean leAudioSupported = result.contains(
                mManager.getProfileManager().getLeAudioProfile());
        boolean classicAudioSupported = result.contains(
                mManager.getProfileManager().getA2dpProfile()) || result.contains(
                mManager.getProfileManager().getHeadsetProfile());
        if (leAudioSupported && classicAudioSupported) {
            result.remove(mManager.getProfileManager().getA2dpProfile());
            result.remove(mManager.getProfileManager().getHeadsetProfile());
        }
        boolean hearingAidSupported = result.contains(
                mManager.getProfileManager().getHearingAidProfile());
        // Remove hearing aids toggle anyway since showing the toggle will confuse users
        if (hearingAidSupported) {
            result.remove(mManager.getProfileManager().getHearingAidProfile());
            if (leAudioSupported
                    && !SystemProperties.getBoolean(BYPASS_LE_AUDIO_ALLOWLIST_PROPERTY, false)
                    && !SystemProperties.getBoolean(
                            LE_AUDIO_TOGGLE_VISIBLE_FOR_ASHA_PROPERTY, true)) {
                result.remove(mManager.getProfileManager().getLeAudioProfile());
            }
        }
        if (leAudioSupported && !classicAudioSupported && !hearingAidSupported) {
            mIsLeAudioOnlyDevice = true;
        }
        Log.d(TAG, "getProfiles:Map:" + mProfileDeviceMap);
        return result;
    }

    private boolean isCurrentDeviceInOrByPassAllowList() {
        if (!SystemProperties.getBoolean(LE_AUDIO_CONNECTION_BY_DEFAULT_PROPERTY, true)) {
            return false;
        }
        return SystemProperties.getBoolean(BYPASS_LE_AUDIO_ALLOWLIST_PROPERTY, false)
                || isModelNameInAllowList(
                BluetoothUtils.getStringMetaData(
                        mCachedDevice.getDevice(), METADATA_MODEL_NAME));
    }

    /**
     * Disable the Le Audio profile for each of the Le Audio devices.
     *
     * @param profile the LeAudio profile
     */
    private void disableLeAudioProfile(LocalBluetoothProfile profile) {
        if (profile == null || mProfileDeviceMap.get(profile.toString()) == null) {
            Log.e(TAG, "There is no the LE profile or no device in mProfileDeviceMap. Do nothing.");
            return;
        }

        mMetricsFeatureProvider.action(
                mContext,
                SettingsEnums.ACTION_BLUETOOTH_PROFILE_LE_AUDIO_OFF,
                isCurrentDeviceInOrByPassAllowList());
        Utils.setLeAudioEnabled(mManager, List.copyOf(mCachedDeviceGroup), false);
    }

    /**
     * Enable the Le Audio profile for each of the Le Audio devices.
     *
     * @param profile the LeAudio profile
     */
    private void enableLeAudioProfile(LocalBluetoothProfile profile) {
        if (profile == null || mProfileDeviceMap.get(profile.toString()) == null) {
            Log.e(TAG, "There is no the LE profile or no device in mProfileDeviceMap. Do nothing.");
            return;
        }

        mMetricsFeatureProvider.action(
                mContext,
                SettingsEnums.ACTION_BLUETOOTH_PROFILE_LE_AUDIO_ON,
                isCurrentDeviceInOrByPassAllowList());
        Utils.setLeAudioEnabled(mManager, List.copyOf(mCachedDeviceGroup), true);
    }

    /**
     * This is a helper method to be called after adding a Preference for a profile. If that
     * profile happened to be A2dp and the device supports high quality audio, it will add a
     * separate preference for controlling whether to actually use high quality audio.
     *
     * @param profile the profile just added
     */
    private void maybeAddHighQualityAudioPref(LocalBluetoothProfile profile) {
        if (!(profile instanceof A2dpProfile)) {
            return;
        }
        BluetoothDevice device = mCachedDevice.getDevice();
        A2dpProfile a2dp = (A2dpProfile) profile;
        if (a2dp.isProfileReady() && a2dp.supportsHighQualityAudio(device)) {
            TwoStatePreference highQualityAudioPref = new SwitchPreferenceCompat(
                    mProfilesContainer.getContext());
            highQualityAudioPref.setKey(HIGH_QUALITY_AUDIO_PREF_TAG);
            highQualityAudioPref.setVisible(false);
            highQualityAudioPref.setOnPreferenceClickListener(clickedPref -> {
                boolean enable = ((TwoStatePreference) clickedPref).isChecked();
                a2dp.setHighQualityAudioEnabled(mCachedDevice.getDevice(), enable);
                return true;
            });
            mProfilesContainer.addPreference(highQualityAudioPref);
        }
    }

    @Override
    public void onPause() {
        for (CachedBluetoothDevice item : mCachedDeviceGroup) {
            item.unregisterCallback(this);
        }
        mProfileManager.removeServiceListener(this);
    }

    @Override
    public void onResume() {
        updateLeAudioConfig();
        for (CachedBluetoothDevice item : mCachedDeviceGroup) {
            item.registerCallback(this);
        }
        mProfileManager.addServiceListener(this);
        refresh();
    }

    private void updateLeAudioConfig() {
        boolean isLeAudioToggleVisible = SystemProperties.getBoolean(
                LE_AUDIO_TOGGLE_VISIBLE_PROPERTY, LE_AUDIO_TOGGLE_VISIBLE_DEFAULT_VALUE);
        boolean isLeEnabledByDefault =
                SystemProperties.getBoolean(LE_AUDIO_CONNECTION_BY_DEFAULT_PROPERTY, true);
        mIsLeAudioToggleEnabled = isLeAudioToggleVisible || isLeEnabledByDefault;
        Log.d(TAG, "LE_AUDIO_TOGGLE_VISIBLE_PROPERTY:" + isLeAudioToggleVisible
                + ", LE_AUDIO_CONNECTION_BY_DEFAULT_PROPERTY:" + isLeEnabledByDefault);
    }

    @Override
    public void onDeviceAttributesChanged() {
        for (CachedBluetoothDevice item : mCachedDeviceGroup) {
            item.unregisterCallback(this);
        }
        mCachedDeviceGroup = Utils.findAllCachedBluetoothDevicesByGroupId(mManager, mCachedDevice);
        for (CachedBluetoothDevice item : mCachedDeviceGroup) {
            item.registerCallback(this);
        }

        super.onDeviceAttributesChanged();
    }

    @Override
    public void onServiceConnected() {
        refresh();
    }

    @Override
    public void onServiceDisconnected() {
        refresh();
    }

    /**
     * Refreshes the state of the switches for all profiles, possibly adding or removing switches as
     * needed.
     */
    @Override
    protected void refresh() {
        ThreadUtils.postOnBackgroundThread(
                () -> {
                    mAdditionalInvisibleProfiles.set(
                            FeatureFactory.getFeatureFactory()
                                    .getBluetoothFeatureProvider()
                                    .getInvisibleProfilePreferenceKeys(
                                            mContext, mCachedDevice.getDevice()));
                    ThreadUtils.postOnMainThread(this::refreshUi);
                });
    }

    private void refreshUi() {
        for (LocalBluetoothProfile profile : getProfiles()) {
            if (profile == null || !profile.isProfileReady()) {
                continue;
            }
            TwoStatePreference pref = mProfilesContainer.findPreference(profile.toString());
            if (pref == null) {
                pref = createProfilePreference(mProfilesContainer.getContext(), profile);
                mProfilesContainer.addPreference(pref);
                maybeAddHighQualityAudioPref(profile);
            }
            refreshProfilePreference(pref, profile);
        }
        for (LocalBluetoothProfile removedProfile : mCachedDevice.getRemovedProfiles()) {
            final TwoStatePreference pref =
                    mProfilesContainer.findPreference(removedProfile.toString());
            if (pref != null) {
                mProfilesContainer.removePreference(pref);
            }
        }

        Preference preference = mProfilesContainer.findPreference(KEY_BOTTOM_PREFERENCE);
        if (preference == null) {
            preference = new Preference(mContext);
            if (mHasExtraSpace) {
                preference.setLayoutResource(R.layout.preference_bluetooth_profile_category);
            } else {
                preference.setLayoutResource(R.layout.preference_category_bluetooth_no_padding);
            }
            preference.setEnabled(false);
            preference.setKey(KEY_BOTTOM_PREFERENCE);
            preference.setOrder(ORDINAL);
            preference.setSelectable(false);
            mProfilesContainer.addPreference(preference);
        }

        Set<String> additionalInvisibleProfiles = mAdditionalInvisibleProfiles.get();
        HashSet<String> combinedInvisibleProfiles = new HashSet<>(mInvisibleProfiles);
        if (additionalInvisibleProfiles != null) {
            combinedInvisibleProfiles.addAll(additionalInvisibleProfiles);
        }
        Log.i(TAG, "Invisible profiles: " + combinedInvisibleProfiles);
        for (int i = 0; i < mProfilesContainer.getPreferenceCount(); ++i) {
            Preference pref = mProfilesContainer.getPreference(i);
            pref.setVisible(pref.isVisible() && !combinedInvisibleProfiles.contains(pref.getKey()));
        }
    }

    @Override
    public String getPreferenceKey() {
        return KEY_PROFILES_GROUP;
    }
}
