/*
 * Copyright 2018 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.car.settings.bluetooth;

import android.annotation.CallSuper;
import android.bluetooth.BluetoothAdapter;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.os.UserManager;

import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceGroup;

import com.android.car.settings.common.FragmentController;
import com.android.settingslib.bluetooth.BluetoothCallback;
import com.android.settingslib.bluetooth.BluetoothDeviceFilter;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LocalBluetoothManager;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * Manages a group of Bluetooth devices by adding preferences for devices that pass a subclass
 * defined filter and removing preferences for devices that no longer pass. Subclasses are
 * dispatched click events on individual preferences to customize the behavior.
 *
 * <p>Note: {@link #refreshUi()} is called whenever a device is added or removed with {@link
 * #onDeviceAdded(CachedBluetoothDevice)} or {@link #onDeviceDeleted(CachedBluetoothDevice)}.
 * Subclasses should listen to state changes (and possibly override additional {@link
 * BluetoothCallback} methods) and call {@link #refreshUi()} for changes which affect their
 * implementation of {@link #getDeviceFilter()}.
 */
public abstract class BluetoothDevicesGroupPreferenceController extends
        BluetoothPreferenceController<PreferenceGroup> {

    private final Map<CachedBluetoothDevice, BluetoothDevicePreference> mPreferenceMap =
            new HashMap<>();
    private final Preference.OnPreferenceClickListener mDevicePreferenceClickListener =
            preference -> {
                onDeviceClicked(((BluetoothDevicePreference) preference).getCachedDevice());
                return true;
            };

    public BluetoothDevicesGroupPreferenceController(Context context, String preferenceKey,
            FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
        super(context, preferenceKey, fragmentController, uxRestrictions);
    }

    @VisibleForTesting
    BluetoothDevicesGroupPreferenceController(Context context, String preferenceKey,
            FragmentController fragmentController, CarUxRestrictions uxRestrictions,
            LocalBluetoothManager localBluetoothManager, UserManager userManager) {
        super(context, preferenceKey, fragmentController, uxRestrictions, localBluetoothManager,
                userManager);
    }

    @Override
    protected Class<PreferenceGroup> getPreferenceType() {
        return PreferenceGroup.class;
    }

    /**
     * Returns a filter for which devices should be included in the group. Devices that do not
     * pass the filter will not be added. Added devices that no longer pass the filter will be
     * removed.
     */
    protected abstract BluetoothDeviceFilter.Filter getDeviceFilter();

    /**
     * Returns a newly created {@link BluetoothDevicePreference} for the given {@link
     * CachedBluetoothDevice}. Subclasses may override this method to customize how devices are
     * represented in the group.
     */
    protected BluetoothDevicePreference createDevicePreference(CachedBluetoothDevice cachedDevice) {
        return new BluetoothDevicePreference(getContext(), cachedDevice,
                shouldShowDisconnectedStateSubtitle());
    }

    /**
     * Returns whether or not the created bluetooth device preferences should display the
     * disconnected state subtitle.
     */
    protected boolean shouldShowDisconnectedStateSubtitle() {
        return true;
    }

    /**
     * Called when a preference in the group is clicked.
     *
     * @param cachedDevice the device represented by the clicked preference.
     */
    protected abstract void onDeviceClicked(CachedBluetoothDevice cachedDevice);

    /**
     * Returns a mapping of all {@link CachedBluetoothDevice} instances represented by this group
     * and their associated preferences.
     */
    protected Map<CachedBluetoothDevice, BluetoothDevicePreference> getPreferenceMap() {
        return mPreferenceMap;
    }

    @Override
    @CallSuper
    protected void updateState(PreferenceGroup preferenceGroup) {
        Collection<CachedBluetoothDevice> cachedDevices =
                getBluetoothManager().getCachedDeviceManager().getCachedDevicesCopy();

        Set<CachedBluetoothDevice> devicesToRemove = new HashSet<>(mPreferenceMap.keySet());
        devicesToRemove.removeAll(cachedDevices);
        for (CachedBluetoothDevice deviceToRemove : devicesToRemove) {
            removePreference(deviceToRemove);
        }

        for (CachedBluetoothDevice cachedDevice : cachedDevices) {
            if (getDeviceFilter().matches(cachedDevice.getDevice())) {
                addPreference(cachedDevice);
            } else {
                removePreference(cachedDevice);
            }
        }

        preferenceGroup.setVisible(preferenceGroup.getPreferenceCount() > 0);
    }

    @Override
    public final void onBluetoothStateChanged(int bluetoothState) {
        super.onBluetoothStateChanged(bluetoothState);
        if (bluetoothState == BluetoothAdapter.STATE_TURNING_OFF) {
            // Cleanup the UI so that we don't have stale representations when the adapter turns
            // on again. This can happen if Bluetooth crashes and restarts.
            getPreference().removeAll();
            mPreferenceMap.clear();
        }
    }

    @Override
    public final void onDeviceAdded(CachedBluetoothDevice cachedDevice) {
        refreshUi();
    }

    @Override
    public final void onDeviceDeleted(CachedBluetoothDevice cachedDevice) {
        refreshUi();
    }

    private void addPreference(CachedBluetoothDevice cachedDevice) {
        if (!mPreferenceMap.containsKey(cachedDevice)) {
            BluetoothDevicePreference devicePreference = createDevicePreference(cachedDevice);
            devicePreference.setOnPreferenceClickListener(mDevicePreferenceClickListener);
            mPreferenceMap.put(cachedDevice, devicePreference);
            getPreference().addPreference(devicePreference);
        }
    }

    private void removePreference(CachedBluetoothDevice cachedDevice) {
        if (mPreferenceMap.containsKey(cachedDevice)) {
            getPreference().removePreference(mPreferenceMap.get(cachedDevice));
            mPreferenceMap.remove(cachedDevice);
        }
    }
}
