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

import static android.app.Activity.RESULT_OK;
import static android.bluetooth.BluetoothGatt.GATT_SUCCESS;
import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH;

import android.app.settings.SettingsEnums;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanRecord;
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.content.Context;
import android.os.Bundle;
import android.os.ParcelUuid;
import android.os.SystemProperties;
import android.util.Log;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.Preference;

import com.android.settings.R;
import com.android.settings.bluetooth.BluetoothDevicePreference;
import com.android.settings.bluetooth.BluetoothProgressCategory;
import com.android.settings.bluetooth.Utils;
import com.android.settings.dashboard.RestrictedDashboardFragment;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.bluetooth.BluetoothCallback;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
import com.android.settingslib.bluetooth.HearingAidInfo;
import com.android.settingslib.bluetooth.HearingAidStatsLogUtils;
import com.android.settingslib.bluetooth.LocalBluetoothManager;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * This fragment shows all scanned hearing devices through BLE scanning. Users can
 * pair them in this page.
 */
public class HearingDevicePairingFragment extends RestrictedDashboardFragment implements
        BluetoothCallback {

    private static final boolean DEBUG = true;
    private static final String TAG = "HearingDevicePairingFragment";
    private static final String BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY =
            "persist.bluetooth.showdeviceswithoutnames";
    private static final String KEY_AVAILABLE_HEARING_DEVICES = "available_hearing_devices";
    // Flags data type from CSS 1.3 Flags
    private static final int BT_DISCOVERABLE_MASK = 0x03;

    LocalBluetoothManager mLocalManager;
    @Nullable
    BluetoothAdapter mBluetoothAdapter;
    @Nullable
    CachedBluetoothDeviceManager mCachedDeviceManager;

    private boolean mShowDevicesWithoutNames;
    @Nullable
    private BluetoothProgressCategory mAvailableHearingDeviceGroup;

    @Nullable
    BluetoothDevice mSelectedDevice;
    final List<BluetoothDevice> mSelectedDeviceList = new ArrayList<>();
    final List<BluetoothGatt> mConnectingGattList = new ArrayList<>();
    final Map<CachedBluetoothDevice, BluetoothDevicePreference> mDevicePreferenceMap =
            new HashMap<>();

    private List<ScanFilter> mLeScanFilters;

    public HearingDevicePairingFragment() {
        super(DISALLOW_CONFIG_BLUETOOTH);
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mLocalManager = Utils.getLocalBtManager(getActivity());
        if (mLocalManager == null) {
            Log.e(TAG, "Bluetooth is not supported on this device");
            return;
        }
        mBluetoothAdapter = getSystemService(BluetoothManager.class).getAdapter();
        mCachedDeviceManager = mLocalManager.getCachedDeviceManager();
        mShowDevicesWithoutNames = SystemProperties.getBoolean(
                BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY, false);

        initPreferencesFromPreferenceScreen();
        initHearingDeviceLeScanFilters();
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        use(ViewAllBluetoothDevicesPreferenceController.class).init(this);
    }

    @Override
    public void onStart() {
        super.onStart();
        if (mLocalManager == null || mBluetoothAdapter == null || isUiRestricted()) {
            return;
        }
        mLocalManager.setForegroundActivity(getActivity());
        mLocalManager.getEventManager().registerCallback(this);
        if (mBluetoothAdapter.isEnabled()) {
            startScanning();
        } else {
            // Turn on bluetooth if it is disabled
            mBluetoothAdapter.enable();
        }
    }

    @Override
    public void onStop() {
        super.onStop();
        if (mLocalManager == null || isUiRestricted()) {
            return;
        }
        stopScanning();
        removeAllDevices();
        for (BluetoothGatt gatt: mConnectingGattList) {
            gatt.disconnect();
        }
        mConnectingGattList.clear();
        mLocalManager.setForegroundActivity(null);
        mLocalManager.getEventManager().unregisterCallback(this);
    }

    @Override
    public boolean onPreferenceTreeClick(Preference preference) {
        if (preference instanceof BluetoothDevicePreference) {
            stopScanning();
            BluetoothDevicePreference devicePreference = (BluetoothDevicePreference) preference;
            mSelectedDevice = devicePreference.getCachedDevice().getDevice();
            if (mSelectedDevice != null) {
                mSelectedDeviceList.add(mSelectedDevice);
            }
            devicePreference.onClicked();
            return true;
        }
        return super.onPreferenceTreeClick(preference);
    }

    @Override
    public void onDeviceDeleted(@NonNull CachedBluetoothDevice cachedDevice) {
        removeDevice(cachedDevice);
    }

    @Override
    public void onBluetoothStateChanged(int bluetoothState) {
        switch (bluetoothState) {
            case BluetoothAdapter.STATE_ON:
                startScanning();
                showBluetoothTurnedOnToast();
                break;
            case BluetoothAdapter.STATE_OFF:
                finish();
                break;
        }
    }

    @Override
    public void onDeviceBondStateChanged(@NonNull CachedBluetoothDevice cachedDevice,
            int bondState) {
        if (DEBUG) {
            Log.d(TAG, "onDeviceBondStateChanged: " + cachedDevice + ", state = "
                    + bondState);
        }
        if (bondState == BluetoothDevice.BOND_BONDED) {
            // If one device is connected(bonded), then close this fragment.
            setResult(RESULT_OK);
            finish();
            return;
        } else if (bondState == BluetoothDevice.BOND_BONDING) {
            // Set the bond entry where binding process starts for logging hearing aid device info
            final int pageId = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider()
                    .getAttribution(getActivity());
            final int bondEntry = AccessibilityStatsLogUtils.convertToHearingAidInfoBondEntry(
                    pageId);
            HearingAidStatsLogUtils.setBondEntryForDevice(bondEntry, cachedDevice);
        }
        if (mSelectedDevice != null) {
            BluetoothDevice device = cachedDevice.getDevice();
            if (mSelectedDevice.equals(device) && bondState == BluetoothDevice.BOND_NONE) {
                // If current selected device failed to bond, restart scanning
                startScanning();
            }
        }
    }

    @Override
    public void onProfileConnectionStateChanged(@NonNull CachedBluetoothDevice cachedDevice,
            int state, int bluetoothProfile) {
        // This callback is used to handle the case that bonded device is connected in pairing list.
        // 1. If user selected multiple bonded devices in pairing list, after connected
        // finish this page.
        // 2. If the bonded devices auto connected in paring list, after connected it will be
        // removed from paring list.
        if (cachedDevice.isConnected()) {
            final BluetoothDevice device = cachedDevice.getDevice();
            if (device != null && mSelectedDeviceList.contains(device)) {
                setResult(RESULT_OK);
                finish();
            } else {
                removeDevice(cachedDevice);
            }
        }
    }

    @Override
    public int getMetricsCategory() {
        return SettingsEnums.HEARING_AID_PAIRING;
    }

    @Override
    protected int getPreferenceScreenResId() {
        return R.xml.hearing_device_pairing_fragment;
    }


    @Override
    protected String getLogTag() {
        return TAG;
    }

    void addDevice(CachedBluetoothDevice cachedDevice) {
        if (mBluetoothAdapter == null) {
            return;
        }
        // Do not create new preference while the list shows one of the state messages
        if (mBluetoothAdapter.getState() != BluetoothAdapter.STATE_ON) {
            return;
        }
        if (mDevicePreferenceMap.get(cachedDevice) != null) {
            return;
        }
        String key = cachedDevice.getDevice().getAddress();
        BluetoothDevicePreference preference = (BluetoothDevicePreference) getCachedPreference(key);
        if (preference == null) {
            preference = new BluetoothDevicePreference(getPrefContext(), cachedDevice,
                    mShowDevicesWithoutNames, BluetoothDevicePreference.SortType.TYPE_FIFO);
            preference.setKey(key);
            preference.hideSecondTarget(true);
        }
        if (mAvailableHearingDeviceGroup != null) {
            mAvailableHearingDeviceGroup.addPreference(preference);
        }
        mDevicePreferenceMap.put(cachedDevice, preference);
        if (DEBUG) {
            Log.d(TAG, "Add device. device: " + cachedDevice);
        }
    }

    void removeDevice(CachedBluetoothDevice cachedDevice) {
        if (DEBUG) {
            Log.d(TAG, "removeDevice: " + cachedDevice);
        }
        BluetoothDevicePreference preference = mDevicePreferenceMap.remove(cachedDevice);
        if (mAvailableHearingDeviceGroup != null && preference != null) {
            mAvailableHearingDeviceGroup.removePreference(preference);
        }
    }

    void startScanning() {
        if (mCachedDeviceManager != null) {
            mCachedDeviceManager.clearNonBondedDevices();
        }
        removeAllDevices();
        startLeScanning();
    }

    void stopScanning() {
        stopLeScanning();
    }

    private final ScanCallback mLeScanCallback = new ScanCallback() {
        @Override
        public void onScanResult(int callbackType, ScanResult result) {
            handleLeScanResult(result);
        }

        @Override
        public void onBatchScanResults(List<ScanResult> results) {
            for (ScanResult result: results) {
                handleLeScanResult(result);
            }
        }

        @Override
        public void onScanFailed(int errorCode) {
            Log.w(TAG, "BLE Scan failed with error code " + errorCode);
        }
    };

    void handleLeScanResult(ScanResult result) {
        if (mCachedDeviceManager == null || !isDeviceDiscoverable(result)) {
            return;
        }
        final BluetoothDevice device = result.getDevice();
        CachedBluetoothDevice cachedDevice = mCachedDeviceManager.findDevice(device);
        if (cachedDevice == null) {
            cachedDevice = mCachedDeviceManager.addDevice(device);
        } else if (cachedDevice.getBondState() == BluetoothDevice.BOND_BONDED) {
            if (DEBUG) {
                Log.d(TAG, "Skip this device, already bonded: " + cachedDevice);
            }
            return;
        }
        if (cachedDevice.getHearingAidInfo() == null) {
            if (DEBUG) {
                Log.d(TAG, "Set hearing aid info on device: " + cachedDevice);
            }
            cachedDevice.setHearingAidInfo(new HearingAidInfo.Builder().build());
        }
        // No need to handle the device if the device is already in the list or discovering services
        if (mDevicePreferenceMap.get(cachedDevice) == null
                && mConnectingGattList.stream().noneMatch(
                        gatt -> gatt.getDevice().equals(device))) {
            if (isAndroidCompatibleHearingAid(result)) {
                addDevice(cachedDevice);
            } else {
                discoverServices(cachedDevice);
            }
        }
    }

    void startLeScanning() {
        if (mBluetoothAdapter == null) {
            return;
        }
        if (DEBUG) {
            Log.v(TAG, "startLeScanning");
        }
        final BluetoothLeScanner leScanner = mBluetoothAdapter.getBluetoothLeScanner();
        if (leScanner == null) {
            Log.w(TAG, "LE scanner not found, cannot start LE scanning");
        } else {
            final ScanSettings settings = new ScanSettings.Builder()
                    .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
                    .setLegacy(false)
                    .build();
            leScanner.startScan(mLeScanFilters, settings, mLeScanCallback);
            if (mAvailableHearingDeviceGroup != null) {
                mAvailableHearingDeviceGroup.setProgress(true);
            }
        }
    }

    void stopLeScanning() {
        if (mBluetoothAdapter == null) {
            return;
        }
        if (DEBUG) {
            Log.v(TAG, "stopLeScanning");
        }
        final BluetoothLeScanner leScanner = mBluetoothAdapter.getBluetoothLeScanner();
        if (leScanner != null) {
            leScanner.stopScan(mLeScanCallback);
            if (mAvailableHearingDeviceGroup != null) {
                mAvailableHearingDeviceGroup.setProgress(false);
            }
        }
    }

    private void removeAllDevices() {
        mDevicePreferenceMap.clear();
        if (mAvailableHearingDeviceGroup != null) {
            mAvailableHearingDeviceGroup.removeAll();
        }
    }

    void initPreferencesFromPreferenceScreen() {
        mAvailableHearingDeviceGroup = findPreference(KEY_AVAILABLE_HEARING_DEVICES);
    }

    private void initHearingDeviceLeScanFilters() {
        mLeScanFilters = new ArrayList<>();
        // Filters for ASHA hearing aids
        mLeScanFilters.add(
                new ScanFilter.Builder().setServiceUuid(BluetoothUuid.HEARING_AID).build());
        mLeScanFilters.add(new ScanFilter.Builder()
                .setServiceData(BluetoothUuid.HEARING_AID, new byte[0]).build());
        // Filters for LE audio hearing aids
        mLeScanFilters.add(new ScanFilter.Builder().setServiceUuid(BluetoothUuid.HAS).build());
        mLeScanFilters.add(new ScanFilter.Builder()
                .setServiceData(BluetoothUuid.HAS, new byte[0]).build());
        // Filters for MFi hearing aids
        mLeScanFilters.add(new ScanFilter.Builder().setServiceUuid(BluetoothUuid.MFI_HAS).build());
        mLeScanFilters.add(new ScanFilter.Builder()
                .setServiceData(BluetoothUuid.MFI_HAS, new byte[0]).build());
    }

    boolean isAndroidCompatibleHearingAid(ScanResult scanResult) {
        ScanRecord scanRecord = scanResult.getScanRecord();
        if (scanRecord == null) {
            if (DEBUG) {
                Log.d(TAG, "Scan record is null, not compatible with Android. device: "
                        + scanResult.getDevice());
            }
            return false;
        }
        List<ParcelUuid> uuids = scanRecord.getServiceUuids();
        if (uuids != null) {
            if (uuids.contains(BluetoothUuid.HEARING_AID) || uuids.contains(BluetoothUuid.HAS)) {
                if (DEBUG) {
                    Log.d(TAG, "Scan record uuid matched, compatible with Android. device: "
                            + scanResult.getDevice());
                }
                return true;
            }
        }
        if (scanRecord.getServiceData(BluetoothUuid.HEARING_AID) != null
                || scanRecord.getServiceData(BluetoothUuid.HAS) != null) {
            if (DEBUG) {
                Log.d(TAG, "Scan record service data matched, compatible with Android. device: "
                        + scanResult.getDevice());
            }
            return true;
        }
        if (DEBUG) {
            Log.d(TAG, "Scan record mismatched, not compatible with Android. device: "
                    + scanResult.getDevice());
        }
        return false;
    }

    void discoverServices(CachedBluetoothDevice cachedDevice) {
        if (DEBUG) {
            Log.d(TAG, "connectGattToCheckCompatibility, device: " + cachedDevice);
        }
        BluetoothGatt gatt = cachedDevice.getDevice().connectGatt(getContext(), false,
                new BluetoothGattCallback() {
                    @Override
                    public void onConnectionStateChange(BluetoothGatt gatt, int status,
                            int newState) {
                        super.onConnectionStateChange(gatt, status, newState);
                        if (DEBUG) {
                            Log.d(TAG, "onConnectionStateChange, status: " + status + ", newState: "
                                    + newState + ", device: " + cachedDevice);
                        }
                        if (status == GATT_SUCCESS
                                && newState == BluetoothProfile.STATE_CONNECTED) {
                            gatt.discoverServices();
                        } else {
                            gatt.disconnect();
                            mConnectingGattList.remove(gatt);
                        }
                    }

                    @Override
                    public void onServicesDiscovered(BluetoothGatt gatt, int status) {
                        super.onServicesDiscovered(gatt, status);
                        if (DEBUG) {
                            Log.d(TAG, "onServicesDiscovered, status: " + status + ", device: "
                                    + cachedDevice);
                        }
                        if (status == GATT_SUCCESS) {
                            if (gatt.getService(BluetoothUuid.HEARING_AID.getUuid()) != null
                                    || gatt.getService(BluetoothUuid.HAS.getUuid()) != null) {
                                if (DEBUG) {
                                    Log.d(TAG, "compatible with Android, device: "
                                            + cachedDevice);
                                }
                                addDevice(cachedDevice);
                            }
                        } else {
                            gatt.disconnect();
                            mConnectingGattList.remove(gatt);
                        }
                    }
                });
        mConnectingGattList.add(gatt);
    }

    void showBluetoothTurnedOnToast() {
        Toast.makeText(getContext(), R.string.connected_device_bluetooth_turned_on_toast,
                Toast.LENGTH_SHORT).show();
    }

    boolean isDeviceDiscoverable(ScanResult result) {
        final ScanRecord scanRecord = result.getScanRecord();
        if (scanRecord == null) {
            return false;
        }
        final int flags = scanRecord.getAdvertiseFlags();
        return (flags & BT_DISCOVERABLE_MASK) != 0;
    }
}
