/*
 * Copyright (c) 2018, Nordic Semiconductor
 *
 * SPDX-License-Identifier: Apache-2.0
 */

package io.runtime.mcumgr.sample.viewmodel;

import android.app.Application;
import android.bluetooth.BluetoothAdapter;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.location.LocationManager;

import java.util.List;

import javax.inject.Inject;

import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;

import org.jetbrains.annotations.NotNull;

import io.runtime.mcumgr.sample.utils.FilterUtils;
import io.runtime.mcumgr.sample.utils.Utils;
import no.nordicsemi.android.support.v18.scanner.BluetoothLeScannerCompat;
import no.nordicsemi.android.support.v18.scanner.ScanCallback;
import no.nordicsemi.android.support.v18.scanner.ScanResult;
import no.nordicsemi.android.support.v18.scanner.ScanSettings;

public class ScannerViewModel extends AndroidViewModel {
    private static final String PREFS_FILTER_UUID_REQUIRED = "filter_uuid";
    private static final String PREFS_FILTER_NEARBY_ONLY = "filter_nearby";

    /** MutableLiveData containing the list of devices. */
    private final DevicesLiveData mDevicesLiveData;
    /** MutableLiveData containing the scanner state. */
    private final ScannerStateLiveData mScannerStateLiveData;

    private final SharedPreferences mPreferences;

    public DevicesLiveData getDevices() {
        return mDevicesLiveData;
    }

    public ScannerStateLiveData getScannerState() {
        return mScannerStateLiveData;
    }

    @Inject
    public ScannerViewModel(@NonNull final Application application,
                            @NonNull final SharedPreferences preferences) {
        super(application);
        mPreferences = preferences;

        final boolean filterUuidRequired = isUuidFilterEnabled();
        final boolean filerNearbyOnly = isNearbyFilterEnabled();

        mScannerStateLiveData = new ScannerStateLiveData(
                Utils.isBleEnabled(),
                Utils.isLocationEnabled(application)
        );
        mDevicesLiveData = new DevicesLiveData(filterUuidRequired, filerNearbyOnly);
        registerBroadcastReceivers(application);
    }

    @Override
    protected void onCleared() {
        super.onCleared();
        getApplication().unregisterReceiver(mBluetoothStateBroadcastReceiver);

        if (Utils.isMarshmallowOrAbove()) {
            getApplication().unregisterReceiver(mLocationProviderChangedReceiver);
        }
    }

    public boolean isUuidFilterEnabled() {
        return mPreferences.getBoolean(PREFS_FILTER_UUID_REQUIRED, true);
    }

    public boolean isNearbyFilterEnabled() {
        return mPreferences.getBoolean(PREFS_FILTER_NEARBY_ONLY, false);
    }

    /**
     * Forces the observers to be notified. This method is used to refresh the screen after the
     * location permission has been granted. In result, the observer in
     * {@link io.runtime.mcumgr.sample.ScannerActivity} will try to start scanning.
     */
    public void refresh() {
        mScannerStateLiveData.refresh();
    }

    /**
     * Updates the device filter. Devices that once passed the filter will still be shown
     * even if they move away from the phone, or change the advertising packet. This is to
     * avoid removing devices from the list.
     *
     * @param uuidRequired if true, the list will display only devices with SMP UUID
     *                     in the advertising packet.
     */
    public void filterByUuid(final boolean uuidRequired) {
        mPreferences.edit().putBoolean(PREFS_FILTER_UUID_REQUIRED, uuidRequired).apply();
        if (mDevicesLiveData.filterByUuid(uuidRequired))
            mScannerStateLiveData.recordFound();
        else
            mScannerStateLiveData.clearRecords();
    }

    /**
     * Updates the device filter. Devices that once passed the filter will still be shown
     * even if they move away from the phone, or change the advertising packet. This is to
     * avoid removing devices from the list.
     *
     * @param nearbyOnly if true, the list will show only devices with high RSSI.
     */
    public void filterByDistance(final boolean nearbyOnly) {
        mPreferences.edit().putBoolean(PREFS_FILTER_NEARBY_ONLY, nearbyOnly).apply();
        if (mDevicesLiveData.filterByDistance(nearbyOnly))
            mScannerStateLiveData.recordFound();
        else
            mScannerStateLiveData.clearRecords();
    }

    /**
     * Start scanning for Bluetooth LE devices.
     */
    public void startScan() {
        if (mScannerStateLiveData.isScanning()) {
            return;
        }

        // Scanning settings
        final ScanSettings settings = new ScanSettings.Builder()
                .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
                .setLegacy(false)
                .setReportDelay(500)
                .setUseHardwareBatchingIfSupported(false)
                .build();

        final BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner();
        scanner.startScan(null, settings, scanCallback);
        mScannerStateLiveData.scanningStarted();
    }

    /**
     * Stop scanning for Bluetooth LE devices.
     */
    public void stopScan() {
        final BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner();
        scanner.stopScan(scanCallback);
        mScannerStateLiveData.scanningStopped();
    }

    private final ScanCallback scanCallback = new ScanCallback() {
        @Override
        public void onScanResult(final int callbackType, @NotNull final ScanResult result) {
            // This callback will be called only if the scan report delay is not set or is set to 0.

            // If the packet has been obtained while Location was disabled, mark Location as not required
            if (Utils.isLocationRequired(getApplication()) && !Utils.isLocationEnabled(getApplication()))
                Utils.markLocationNotRequired(getApplication());

            if (!isNoise(result) && mDevicesLiveData.deviceDiscovered(result)) {
                mDevicesLiveData.applyFilter();
                mScannerStateLiveData.recordFound();
            }
        }

        @Override
        public void onBatchScanResults(@NotNull final List<ScanResult> results) {
            // This callback will be called only if the report delay set above is greater then 0.

            // If the packet has been obtained while Location was disabled, mark Location as not required
            if (Utils.isLocationRequired(getApplication()) && !Utils.isLocationEnabled(getApplication()))
                Utils.markLocationNotRequired(getApplication());

            boolean atLeastOneMatchedFilter = false;
            for (final ScanResult result : results)
                atLeastOneMatchedFilter =
                        (!isNoise(result) && mDevicesLiveData.deviceDiscovered(result))
                                || atLeastOneMatchedFilter;
            if (atLeastOneMatchedFilter) {
                mDevicesLiveData.applyFilter();
                mScannerStateLiveData.recordFound();
            }
        }

        @Override
        public void onScanFailed(final int errorCode) {
            // TODO This should be handled
            stopScan();
            mScannerStateLiveData.scanningStopped();
        }
    };

    /**
     * Register for required broadcast receivers.
     */
    private void registerBroadcastReceivers(@NonNull final Application application) {
        application.registerReceiver(mBluetoothStateBroadcastReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));
        if (Utils.isMarshmallowOrAbove()) {
            application.registerReceiver(mLocationProviderChangedReceiver, new IntentFilter(LocationManager.MODE_CHANGED_ACTION));
        }
    }

    /**
     * Broadcast receiver to monitor the changes in the location provider
     */
    private final BroadcastReceiver mLocationProviderChangedReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(final Context context, final Intent intent) {
            final boolean enabled = Utils.isLocationEnabled(context);
            mScannerStateLiveData.setLocationEnabled(enabled);
        }
    };

    /**
     * Broadcast receiver to monitor the changes in the bluetooth adapter
     */
    private final BroadcastReceiver mBluetoothStateBroadcastReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(final Context context, final Intent intent) {
            final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF);
            final int previousState = intent.getIntExtra(BluetoothAdapter.EXTRA_PREVIOUS_STATE, BluetoothAdapter.STATE_OFF);

            switch (state) {
                case BluetoothAdapter.STATE_ON:
                    mScannerStateLiveData.bluetoothEnabled();
                    break;
                case BluetoothAdapter.STATE_TURNING_OFF:
                case BluetoothAdapter.STATE_OFF:
                    if (previousState != BluetoothAdapter.STATE_TURNING_OFF && previousState != BluetoothAdapter.STATE_OFF) {
                        stopScan();
                        mDevicesLiveData.bluetoothDisabled();
                        mScannerStateLiveData.bluetoothDisabled();
                    }
                    break;
            }
        }
    };

    /**
     * This method returns true if the scan result may be considered as noise.
     * This is to make the device list on the scanner screen shorter.
     * <p>
     * This implementation considers as noise devices that:
     * <ul>
     * <li>Are not connectable (Android Oreo or newer only),</li>
     * <li>Are far away (RSSI < -80),</li>
     * <li>Advertise as beacons (iBeacons, Nordic Beacons, Microsoft Advertising Beacons,
     * Eddystone,</li>
     * <li>Advertise with AirDrop footprint,</li>
     * <li>Advertise as Bluetooth Mesh devices.</li>
     * </ul>
     * Noise devices will no the shown on the scanner screen even with all filters disabled.
     *
     * @param result the scan result.
     * @return true, if the device may be dismissed, false otherwise.
     */
    @SuppressWarnings({"BooleanMethodIsAlwaysInverted", "RedundantIfStatement"})
    private boolean isNoise(@NonNull final ScanResult result) {
        // Do not show non-connectable devices.
        // Only Android Oreo or newer can say if a device is connectable. On older Android versions
        // the Support Scanner Library assumes all devices are connectable (compatibility mode).
        if (!result.isConnectable())
            return true;

        // Very distant devices are noise.
        if (result.getRssi() < -80)
            return true;

        if (FilterUtils.isBeacon(result))
            return true;

        if (FilterUtils.isAirDrop(result))
            return true;

        if (FilterUtils.isMeshDevice(result))
            return true;

        return false;
    }
}