package com.siliconlabs.bledemo.ble;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanSettings;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Handler;
import android.os.ParcelUuid;
import android.preference.PreferenceManager;

import androidx.annotation.Nullable;

import android.text.TextUtils;
import android.util.Log;

import com.siliconlabs.bledemo.bluetoothdatamodel.parsing.ScanRecordParser;
import com.siliconlabs.bledemo.log.CommonLog;
import com.siliconlabs.bledemo.log.ConnectionStateChangeLog;
import com.siliconlabs.bledemo.log.DisconnectByButtonLog;
import com.siliconlabs.bledemo.log.ServicesDiscoveredLog;
import com.siliconlabs.bledemo.utils.Constants;
import com.siliconlabs.bledemo.utils.LocalService;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

import timber.log.Timber;

/**
 * Service handling Bluetooth (regular and BLE) communcations.
 */
public class BlueToothService extends LocalService<BlueToothService> {

    // If connection is not successfully established or error message received after CONNECTION_TIMEOUT milliseconds.
    private static final int CONNECTION_TIMEOUT = 15000;
    // If a scan of one device's services takes more than SCAN_DEVICE_TIMEOUT milliseconds, cancel it.
    private static final int SCAN_DEVICE_TIMEOUT = 4000;

    private static final String PREF_KEY_SAVED_DEVICES = "_pref_key_saved_devs_";
    private static final String TAG = BlueToothService.class.getSimpleName();

    public static abstract class Binding extends LocalService.Binding<BlueToothService> {

        public Binding(Context context) {
            super(context);
        }

        @Override
        protected Class<BlueToothService> getServiceClass() {
            return BlueToothService.class;
        }
    }

    public enum GattConnectType {
        THERMOMETER, LIGHT, RANGE_TEST
    }

    public static class Receiver extends BroadcastReceiver {

        static final AtomicInteger currentState = new AtomicInteger(0);
        static final List<BlueToothService> registeredServices = new ArrayList<>();

        @Override
        public void onReceive(Context context, Intent intent) {
            final String action = intent.getAction();

            if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) {
                final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, 0);
                synchronized (currentState) {
                    currentState.set(state);

                    for (BlueToothService blueToothService : registeredServices) {
                        blueToothService.notifyBluetoothStateChange(state);
                    }
                }
            } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
                boolean restart = false;

                for (BlueToothService blueToothService : registeredServices) {
                    if (blueToothService.notifyDiscoverFinished()) {
                        restart = true;
                    }
                }

                if (restart) {
                    registeredServices.get(0).bluetoothAdapter.startDiscovery();
                }
            }
        }
    }

    public interface Listener {

        /**
         * If this returns a non-empty list, scanned devices will be matched by the
         * return filters. A device will be scanned and recognized if it advertisement matches
         * one of more filters returned by this method.
         * <p/>
         * An empty list (or null) will match every bluetooth device, i.e. no filtering.
         *
         * @return The filters by which the BLE scanning will be filtered.
         */
        List<ScanFilterCompat> getScanFilters();

        /**
         * This method should get the user's permission (at least the first time around) to
         * enable the bluetooth automatically when necessary.
         *
         * @return True only if the user allows the code to automatically enable the bluetooth adapter.
         */
        boolean askForEnablingBluetoothAdapter();

        /**
         * Called when the Bluetooth-adapter state changes.
         *
         * @param bluetoothAdapterState State of the adapter.
         */
        void onStateChanged(int bluetoothAdapterState);

        /**
         * Called when a discovery of bluetooth devices has started.
         */
        void onScanStarted();

        /**
         * Called when a new bluetooth device has ben discovered or when an already discovered bluetooth
         * device's information has been updated.
         *
         * @param devices           List of all bluetooth devices currently discovered by the scan since {@link #onScanStarted()}.
         * @param changedDeviceInfo Indicates which device in 'devices' is new or updated (can be ignored).
         */
        void onScanResultUpdated(List<BluetoothDeviceInfo> devices, BluetoothDeviceInfo changedDeviceInfo);

        /**
         * Called when the current discovery process has ended.
         * Note that this method may be called more than once after a call to {@link #onScanStarted()}.
         */
        void onScanEnded();

        /**
         * Called when a interesting device is ready to be used for communication (it is connected and ready).
         * It is possible that after this method is called with a non-null device parameter, it can be called
         * with a null device parameter value later when the device gets disconnected or some other
         * error occurs.
         *
         * @param device Device that is the currently selected device or null if something went wrong.
         */
        void onDeviceReady(BluetoothDevice device, boolean isInteresting);

        /**
         * Called when a device is connected and one of its characteristics has changed.
         *
         * @param characteristic The characteristic.
         * @param value          The new value of the characteristic.
         */
        void onCharacteristicChanged(GattCharacteristic characteristic, Object value);
    }

    private boolean isDestroyed;

    private Handler handler;
    BluetoothAdapter bluetoothAdapter;
    private BluetoothManager bluetoothManager;
    private final AtomicReference<BluetoothDevice> knownDevice = new AtomicReference<>();

    private final Map<String, BluetoothDeviceInfo> discoveredDevices = new LinkedHashMap<>();
    private final Map<String, BluetoothDeviceInfo> interestingDevices = new LinkedHashMap<>();

    private final AtomicInteger currentState = Receiver.currentState;
    private int prevBluetoothState;
    private final Listeners listeners = new Listeners();

    @SuppressWarnings("PointlessBooleanExpression")
    private boolean useBLE = true;

    private BluetoothServer bluetoothServer;
    private BluetoothClient bluetoothClient;
    private BluetoothLEGatt bluetoothLEGatt;
    private BluetoothGatt bluetoothGatt;
    private TimeoutGattCallback extraCallback;
    public Map<String, BluetoothGatt> gattMap = new HashMap<>();

    private final BroadcastReceiver mReceiver = new BScanCallback(this);
    private Object bleScannerCallback;

    private boolean discoveryStarted;


    private final Runnable scanTimeout = new Runnable() {
        @Override
        public void run() {
            stopScanning();
        }
    };
    private final static long RSSI_UPDATE_FREQ = 2000;
    private final Runnable rssiUpdate = new Runnable() {
        @Override
        public void run() {
            if (bluetoothGatt != null) {
                bluetoothGatt.readRemoteRssi();
                handler.postDelayed(this, RSSI_UPDATE_FREQ);
            }
        }
    };

    private final Runnable connectionTimeout = new Runnable() {
        @Override
        public void run() {
            if (bluetoothGatt != null) {
                Log.d("timeout", "called");
                bluetoothGatt.disconnect();
                bluetoothGatt.close();
                if (extraCallback != null) {
                    extraCallback.onTimeout();
                }
            }
        }
    };

    private boolean isBTAdapterAlreadyBeingEnabled;

    /**
     * Preference whose {@link #PREF_KEY_SAVED_DEVICES} key will have a set addresses of known devices.
     * Note that this list only grows... since we don't expect a phone/device to come into contact with many
     * interesting devices, this is fine. In the future we may want to back this by a LRU list or something similar to purge
     * device-addresses that have been used a long time ago.
     */
    private SharedPreferences savedInterestingDevices;

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

        bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
        bluetoothAdapter = bluetoothManager.getAdapter();

        if (bluetoothAdapter == null) {
            // TODO Device does not support bluetooth at all.
            stopSelf();
            return;
        }

        if (useBLE) {
            useBLE = getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE);
        }

        savedInterestingDevices = PreferenceManager.getDefaultSharedPreferences(this);

        handler = new Handler();

        knownDevice.set(null);
        discoveredDevices.clear();
        interestingDevices.clear();

        synchronized (currentState) {
            currentState.set(bluetoothAdapter.getState());
        }

        // Register the BroadcastReceiver
        IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
        registerReceiver(mReceiver, filter); // Don't forget to unregister during onDestroy

        Receiver.registeredServices.add(this);
    }

    @Override
    public void onDestroy() {
        isDestroyed = true;

        handler.removeCallbacks(scanTimeout);
        stopScanning();
        stopDiscovery();
        clearGatt();

        unregisterReceiver(mReceiver);
        Receiver.registeredServices.remove(this);

        if (bluetoothClient != null) {
            bluetoothClient.cancel();
        }

        if (bluetoothServer != null) {
            bluetoothServer.cancel();
        }

        if (bluetoothLEGatt != null) {
            bluetoothLEGatt.cancel();
        }

        super.onDestroy();
    }

    public void addListener(Listener listener) {
        synchronized (currentState) {
            if (!listeners.contains(listener)) {
                notifyInitialStateForListener(listener);
                listeners.add(listener);
            }
        }
    }

    private void notifyInitialStateForListener(final Listener listener) {
        handler.post(new Runnable() {
            @Override
            public void run() {
                int state = Receiver.currentState.get();

                if (state == BluetoothAdapter.STATE_ON) {
                    listener.onStateChanged(state);

                    if (discoveryStarted) {
                        listener.onScanStarted();
                    }
                } else {
                    listener.onStateChanged(state);
                }
            }
        });
    }

    public void removeListener(Listener listener) {
        synchronized (currentState) {
            listeners.remove(listener);
        }
    }

    /**
     * Reads the value of a characteristic of the currently connected device.
     *
     * @param characteristic Characteristic whose value will be read.
     */
    public void read(GattCharacteristic characteristic) {
        if (bluetoothLEGatt == null) {
            listeners.onCharacteristicChanged(characteristic, null);
            return;
        }

        bluetoothLEGatt.read(characteristic.number);
    }

    /**
     * This method discovers which devices of interest are available.
     * <p/>
     * If this method returns true (discovery started), the caller should wait for the onScanXXX methods of
     * {@link BlueToothService.Listener}.
     *
     * @param clearCache True if the cache/list of the currently discovered devices should be cleared.
     * @return True if discovery started.
     */
    public boolean discoverDevicesOfInterest(boolean clearCache) {
        if (!bluetoothAdapter.isEnabled()) {
//            if (!isBTAdapterAlreadyBeingEnabled && listeners.askForEnablingBluetoothAdapter()) {
//                isBTAdapterAlreadyBeingEnabled = bluetoothAdapter.enable();
//            }
//            return false;
            return false;
        }

        isBTAdapterAlreadyBeingEnabled = false;

        discoveryStarted = true;
        listeners.onScanStarted();

        if (clearCache) {
            synchronized (discoveredDevices) {
                discoveredDevices.clear();
                interestingDevices.clear();
            }
        }

        startDiscovery();
        return true;
    }

    public void clearCache() { //TODO Created to clean services cache

        synchronized (discoveredDevices) {
            discoveredDevices.clear();
            interestingDevices.clear();
        }
    }

    /**
     * This method either starts the device if the device is known or discovers which devices of interest are available.
     * <p/>
     * If this method returns true (device is known), the caller should wait for the
     * {@link BlueToothService.Listener#notifyDeviceReady(BluetoothDevice, boolean)} callback.
     * If this method return false (discovery is started), the caller should wait for the onScanXXX methods of
     * {@link BlueToothService.Listener}.
     *
     * @param clearCache True if the cache/list of the currently discovered devices should be cleared.
     * @return True if device is known and is starting. False if it is not known and discovery is started.
     */
    public boolean startOrDiscoverDeviceOfInterest(boolean clearCache) {
        if (!bluetoothAdapter.isEnabled()) {
//            if (!isBTAdapterAlreadyBeingEnabled && listeners.askForEnablingBluetoothAdapter()) {
//                isBTAdapterAlreadyBeingEnabled = bluetoothAdapter.enable();
//            }
//            return false;
            return false;
        }

        isBTAdapterAlreadyBeingEnabled = false;

        if (!restartInterestingDevice()) {
            discoveryStarted = true;
            listeners.onScanStarted();

            if (clearCache) {
                synchronized (discoveredDevices) {
                    discoveredDevices.clear();
                    interestingDevices.clear();
                }
            }

            startDiscovery();
            return false;
        }
        return true;
    }

    public void stopDiscoveringDevices(boolean clearCache) {
        if (discoveryStarted) {
            if (clearCache) {
                synchronized (discoveredDevices) {
                    discoveredDevices.clear();
                    interestingDevices.clear();
                }
            }

            stopDiscovery();
            if (!scanDiscoveredDevices()) {
                onScanningCanceled();
            }
        }
    }

    /**
     * If the call {@link #startOrDiscoverDeviceOfInterest(boolean)} returned true, a device is currently connected or about to be connected.
     * This method will disconnected from the currently connected device or about any ongoing attempt to connect.
     */
    public void stopConnectedDevice() {
        if (bluetoothLEGatt != null) {
            bluetoothLEGatt.cancel();
            bluetoothLEGatt = null;
        }
    }

    private void startDiscovery() {
        if (bluetoothAdapter == null) {
            bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
            bluetoothAdapter = bluetoothManager.getAdapter();
        }

        handler.removeCallbacks(scanTimeout);

        if (useBLE) {
            ScanCallback scannerCallback = new BLEScanCallbackLollipop(this);
            bleScannerCallback = scannerCallback;

            if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                ScanSettings settings = new ScanSettings.Builder()
                        .setLegacy(false)
                        .setReportDelay(0)
                        .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build();
                bluetoothAdapter.getBluetoothLeScanner().startScan((List<ScanFilter>) listeners.getScanFilterL(), settings, scannerCallback);
            } else {
                ScanSettings settings = new ScanSettings.Builder()
                        .setReportDelay(0)
                        .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build();
                bluetoothAdapter.getBluetoothLeScanner().startScan((List<ScanFilter>) listeners.getScanFilterL(), settings, scannerCallback);
            }

        } else {
            if (!bluetoothAdapter.startDiscovery()) {
                onDiscoveryCanceled();
            }
        }
    }

    void onDiscoveryCanceled() {
        if (discoveryStarted) {
            discoveryStarted = false;
            listeners.onScanEnded();
        }
    }

    private void stopDiscovery() {
        if (bluetoothAdapter == null) {
            return;
        }

        if (useBLE) {
            if (bluetoothAdapter.getBluetoothLeScanner() == null) {
                return;
            }

            if (bleScannerCallback != null) {
                bluetoothAdapter.getBluetoothLeScanner().stopScan((ScanCallback) bleScannerCallback);
            }
        } else if (bluetoothAdapter.isDiscovering()) {
            bluetoothAdapter.cancelDiscovery();
        }
    }

    private void stopScanning() {
        Collection<BluetoothLEGatt> leGattsToClose;
        synchronized (discoveredDevices) {
            leGattsToClose = new ArrayList<>(discoveredDevices.size());
            for (BluetoothDeviceInfo devInfo : discoveredDevices.values()) {
                leGattsToClose.add((BluetoothLEGatt) devInfo.gattHandle);
            }
        }

        BluetoothLEGatt.cancelAll(leGattsToClose);
    }

    public void setKnownDevice(BluetoothDevice device) {
        knownDevice.set(device);
    }

    /**
     * If the device of interest is known, this method will try to restart it (reconnect to it and get the appropriate services).
     * If this method returns true, the caller should wait for the {@link BlueToothService.Listener#onDeviceReady(BluetoothDevice, boolean)} callback.
     *
     * @return True if the device is known. False if the device is not known.
     */
    public boolean restartInterestingDevice() { //It was private
        final BluetoothDevice btDevice = knownDevice.get();

        if (btDevice == null) {
            return false;
        }

        if (useBLE && (bluetoothLEGatt != null) && bluetoothLEGatt.representsConnectedDevice(btDevice)) {
            if (!discoveryStarted) {
                listeners.onScanStarted();
                listeners.onScanEnded();
                listeners.onDeviceReady(btDevice, bluetoothLEGatt.isOfInterest);
            }
            return true;
        }

        discoveryStarted = true;
        listeners.onScanStarted();

        if (useBLE) {
            if (bluetoothLEGatt != null) {
                bluetoothLEGatt.cancel();
            }

            bluetoothLEGatt = new BluetoothLEGatt(this, btDevice) {
                private BluetoothDeviceInfo deviceInfo;

                {
                    deviceInfo = getBluetoothDeviceInfo(btDevice);
                    deviceInfo.areServicesBeingDiscovered = true;
                    notifyUpdateDevices(deviceInfo);
                }

                @Override
                protected void setGattServices(List<BluetoothGattService> services) {
                    super.setGattServices(services);

                    updateDiscoveredDevice(deviceInfo, interestingServices, false);

                    if (isOfInterest) {
                        notifyDeviceReady(device, true);
                    } else {
                        close();
                        notifyDeviceReady(device, false);
                    }
                }

                @Override
                protected void notifyCharacteristicChanged(final int characteristicID, final Object value) {
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            final GattCharacteristic characteristic = GATT_CHARACTER_DESCS.get(characteristicID);
                            listeners.onCharacteristicChanged(characteristic, value);
                        }
                    });
                }
            };
        } else {
            // TODO get service-uuid from string-resource.
            bluetoothClient = new BluetoothClient(bluetoothAdapter, btDevice, UUID.fromString("276b2885-3d2e-403d-a36d-d40374bfbc52"));
            bluetoothClient.start();
        }

        return true;
    }

    void notifyBluetoothStateChange(final int newState) {
        if (newState == BluetoothAdapter.STATE_TURNING_OFF) {
            stopScanning();
            stopConnectedDevice();
        }

        handler.post(new Runnable() {
            @Override
            public void run() {
                if (prevBluetoothState != newState) {
                    if (newState == BluetoothAdapter.STATE_OFF) {
                        if (discoveryStarted) {
                            discoveryStarted = false;
                            listeners.onScanEnded();
                        }

                        synchronized (discoveredDevices) {
                            discoveredDevices.clear();
                            interestingDevices.clear();
                        }

                        setKnownDevice(null);
                        listeners.onDeviceReady(null, false);
                        listeners.onStateChanged(newState);
                    } else if (newState == BluetoothAdapter.STATE_ON) {
                        listeners.onStateChanged(newState);
                        isBTAdapterAlreadyBeingEnabled = false;
                    } else {
                        listeners.onStateChanged(newState);
                    }

                    prevBluetoothState = newState;
                }
            }
        });
    }

    boolean notifyDiscoverFinished() {
        if (useBLE) {
            return false;
        }

        boolean continueScanning = !isDestroyed && (knownDevice.get() == null);

        if (!continueScanning) {
            if (discoveryStarted) {
                discoveryStarted = false;
                listeners.onScanEnded();
            }
        }

        return continueScanning;
    }

    void notifyDeviceReady(final BluetoothDevice device, final boolean isInteresting) {
        handler.post(new Runnable() {
            @Override
            public void run() {
                setKnownDevice(isInteresting ? device : null);
                listeners.onDeviceReady(device, isInteresting);
            }
        });
    }

    boolean addDiscoveredDevice(ScanResultCompat result) {


        Log.d(TAG, "addDiscoveredDevice: " + result);


        if (knownDevice.get() != null) {
            return true;
        }

        final ArrayList<BluetoothDeviceInfo> listenerResult;
        final BluetoothDeviceInfo listenerChanged;
        final boolean postNewDiscoveryTimeout;

        BluetoothDevice device = result.getDevice();
        BluetoothDeviceInfo devInfo;
        synchronized (discoveredDevices) {
            final String address = device.getAddress();

            devInfo = discoveredDevices.get(address);
            if (devInfo == null) {
                devInfo = new BluetoothDeviceInfo();
                devInfo.device = device;
                discoveredDevices.put(address, devInfo);
                postNewDiscoveryTimeout = true;
            } else {
                devInfo.device = device;
                postNewDiscoveryTimeout = false;
            }
            devInfo.scanInfo = result;

            if (!devInfo.isConnectable) {
                devInfo.isConnectable = result.isConnectable();
            }

            devInfo.count++;

            if (devInfo.timestampLast == 0) {
                devInfo.timestampLast = result.getTimestampNanos();
            } else {
                devInfo.setIntervalIfLower(result.getTimestampNanos() - devInfo.timestampLast);
                devInfo.timestampLast = result.getTimestampNanos();
            }

            devInfo.rawData = ScanRecordParser.getRawAdvertisingDate(result.getScanRecord().getBytes());

            if (isDeviceInteresting(device)) {
                devInfo.isNotOfInterest = false;
                devInfo.isOfInterest = true;
                if (!interestingDevices.containsKey(address)) {
                    interestingDevices.put(address, devInfo);
                }
            } else if (isDeviceNotInteresting(device)) {
                devInfo.isNotOfInterest = true;
                devInfo.isOfInterest = false;
            }

            if (!listeners.isEmpty()) {
                listenerResult = new ArrayList<>(discoveredDevices.size());
                listenerChanged = devInfo.clone();
                for (BluetoothDeviceInfo di : discoveredDevices.values()) {
                    if (!isDeviceNotInteresting(di.device)) {
                        listenerResult.add(di.clone());
                    }
                }
            } else {
                listenerResult = null;
                listenerChanged = null;
            }
        }

        if (listenerResult != null) {
            handler.post(new Runnable() {
                @Override
                public void run() {
                    listeners.onScanResultUpdated(listenerResult, listenerChanged);
                }
            });
        }

        return false;
    }

    /**
     * Returns true if device is of interest.
     * Returns false when not sure.
     *
     * @param device The device
     * @return True if device is a of interest.
     */
    private boolean isDeviceInteresting(BluetoothDevice device) {
        if (true) return true;

        Set<String> devices = savedInterestingDevices.getStringSet(PREF_KEY_SAVED_DEVICES, null);
        if ((devices != null) && devices.contains(device.getAddress())) {
            return true;
        }

        String name = device.getName();
        return (!TextUtils.isEmpty(name) && name.toLowerCase().contains("blue gecko"));
    }

    /**
     * Returns true if device is certainly not interesting
     * Returns false when not sure.
     *
     * @param device The device
     * @return True if the device is not interesting.
     */
    private boolean isDeviceNotInteresting(BluetoothDevice device) {
        if (true) return false;

        String name = device.getName();
        return (TextUtils.isEmpty(name) || !name.toLowerCase().contains("blue gecko"));
    }

    boolean scanDiscoveredDevices() {
        Timber.d("scanDiscoveredDevices called.");
        Log.d("scanDiscoveredDevices", "called");
        handler.removeCallbacks(scanTimeout);
        handler.postDelayed(scanTimeout, SCAN_DEVICE_TIMEOUT);

        if (knownDevice.get() != null) {
            return false;                       //TODO It was commented to try to clean Services Cache
        }

        BluetoothDeviceInfo devInfo = null;
        synchronized (discoveredDevices) {
            final Collection<BluetoothDeviceInfo> devices = discoveredDevices.values();
            for (BluetoothDeviceInfo di : devices) {
                if (di.isUnDiscovered()) {
                    devInfo = di;
                    break;
                }
            }

            if (devInfo == null) {
                Timber.d("scanDiscoveredDevices called: Nothing left!");
                Log.d("scanDiscoveredDevices", "called: Nothing left!");
                return false;
            }

            final BluetoothDeviceInfo devInfoForDiscovery = devInfo;
            devInfoForDiscovery.discover(new BluetoothLEGatt(this, devInfoForDiscovery.device) {
                @Override
                protected void setGattServices(List<BluetoothGattService> services) {
                    super.setGattServices(services);

                    close();

                    updateDiscoveredDevice(devInfoForDiscovery, interestingServices, true);
                }
            });
        }

        Timber.d("scanDiscoveredDevices called: Next up is " + devInfo.device.getAddress());
        Log.d("scanDiscoveredDevices", " called: Next up is " + devInfo.device.getAddress());
        return true;
    }

    void onScanningCanceled() {
        handler.removeCallbacks(scanTimeout);
        if (discoveryStarted) {
            discoveryStarted = false;
            listeners.onScanEnded();
        }
    }

    void notifyUpdateDevices(BluetoothDeviceInfo devInfo) {
        final ArrayList<BluetoothDeviceInfo> listenerResult;
        final BluetoothDeviceInfo listenerChanged;

        synchronized (discoveredDevices) {
            if (!listeners.isEmpty()) {
                listenerResult = new ArrayList<>(discoveredDevices.size());
                listenerChanged = devInfo.clone();
                for (BluetoothDeviceInfo di : discoveredDevices.values()) {
                    if (!isDeviceNotInteresting(di.device)) {
                        listenerResult.add(di.clone());
                    }
                }
            } else {
                listenerResult = null;
                listenerChanged = null;
            }
        }

        handler.post(new Runnable() {
            @Override
            public void run() {
                if (listenerResult != null) {
                    listeners.onScanResultUpdated(listenerResult, listenerChanged);
                }
            }
        });
    }

    void updateDiscoveredDevice(BluetoothDeviceInfo devInfo, List<BluetoothGattService> services, final boolean keepScanning) {
        final ArrayList<BluetoothDeviceInfo> listenerResult;
        final BluetoothDeviceInfo listenerChanged;

        synchronized (discoveredDevices) {
            devInfo.gattHandle = null;

            if (services == null) {
                devInfo.serviceDiscoveryFailed = true;
            } else {
                devInfo.serviceDiscoveryFailed = false;
                devInfo.isOfInterest = !services.isEmpty();
                devInfo.isNotOfInterest = services.isEmpty();
            }
            devInfo.areServicesBeingDiscovered = false;

            if (devInfo.isOfInterest) {
                interestingDevices.put(devInfo.device.getAddress(), devInfo);

                Set<String> devices = savedInterestingDevices.getStringSet(PREF_KEY_SAVED_DEVICES, null);
                Set<String> knownDevices = (devices == null) ? new HashSet<String>() : new HashSet<>(devices);
                knownDevices.add(devInfo.device.getAddress());
                savedInterestingDevices.edit().putStringSet(PREF_KEY_SAVED_DEVICES, knownDevices).apply();
            }

            if (!listeners.isEmpty()) {
                listenerResult = new ArrayList<>(discoveredDevices.size());
                listenerChanged = devInfo.clone();
                for (BluetoothDeviceInfo di : discoveredDevices.values()) {
                    if (!isDeviceNotInteresting(di.device)) {
                        listenerResult.add(di.clone());
                    }
                }
            } else {
                listenerResult = null;
                listenerChanged = null;
            }
        }

        handler.post(new Runnable() {
            @Override
            public void run() {
                boolean resumeScanning = !isDestroyed && keepScanning;
                if (resumeScanning) {
                    resumeScanning = scanDiscoveredDevices();
                }

                if (listenerResult != null) {
                    listeners.onScanResultUpdated(listenerResult, listenerChanged);
                }
                if (!resumeScanning) {
                    onScanningCanceled();
                }
            }
        });
    }

    BluetoothDeviceInfo getBluetoothDeviceInfo(BluetoothDevice device) {
        BluetoothDeviceInfo devInfo = null;
        synchronized (discoveredDevices) {
            for (BluetoothDeviceInfo deviceInfo : discoveredDevices.values()) {
                if (deviceInfo.device.getAddress().equals(device.getAddress())) {
                    devInfo = deviceInfo;
                    break;
                }
            }

            if (devInfo == null) {
                devInfo = new BluetoothDeviceInfo();
                devInfo.device = device;
                devInfo.scanInfo = new ScanResultCompat();
                devInfo.scanInfo.setDevice(device);
                ScanRecordCompat record = new ScanRecordCompat();
                record.setTxPowerLevel(Integer.MIN_VALUE);
                record.setAdvertiseFlags(-1);
                devInfo.scanInfo.setScanRecord(record);

                discoveredDevices.put(device.getAddress(), devInfo);
            }
        }

        return devInfo;
    }

    public boolean disconnectGatt(String deviceAddress) {
        boolean disconnect = false;
        if (bluetoothGatt != null && bluetoothGatt.getDevice().getAddress().equals(deviceAddress)) { //todo po usunieciu blueGatt to usuniemy, zostanie sama mapagatt
            disconnect = true;
        } else {
            bluetoothGatt = gattMap.get(deviceAddress);
            if (bluetoothGatt != null) {
                disconnect = true;
            }
        }
        if (disconnect) {
            clearGatt(bluetoothGatt);
            bluetoothGatt = null;
            if (gattMap.containsKey(deviceAddress)) {
                clearGatt(gattMap.get(deviceAddress));
                gattMap.remove(deviceAddress);
            }
            handler.removeCallbacks(rssiUpdate);
            handler.removeCallbacks(connectionTimeout);
//            bluetoothGatt.getDevice().fetchUuidsWithSdp();
            Constants.LOGS.add(new DisconnectByButtonLog(deviceAddress));
            return true;
        }
        return false;
    }

    private void clearGatt(BluetoothGatt bluetoothGatt) {
        bluetoothGatt.disconnect();
        bluetoothGatt.close();
    }

    public void clearGatt() {
        handler.removeCallbacks(rssiUpdate);
        handler.removeCallbacks(connectionTimeout);
        if (bluetoothGatt != null) {
            Log.d("clearGatt", "called");
            bluetoothGatt.disconnect();
            if (bluetoothGatt != null) {
                bluetoothGatt.close();
            }
            bluetoothGatt = null;
        }
    }

    public void registerGattCallback(boolean requestRssiUpdates, TimeoutGattCallback callback) {
        if (requestRssiUpdates) {
            handler.post(rssiUpdate);
        } else {
            handler.removeCallbacks(rssiUpdate);
        }
        extraCallback = callback;
    }

    public void discoverGattServices() {
        if (bluetoothGatt != null) {
            bluetoothGatt.discoverServices();
        }
    }

    public void refreshGattServices() {
        if (bluetoothGatt != null) {
            refreshGattDB(bluetoothGatt);
        }
    }

    private void refreshGattDB(final BluetoothGatt gatt) {
        while (!refreshDeviceCache(gatt)) ;
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                gatt.discoverServices();
            }
        }, 500);
    }

    private boolean refreshDeviceCache(BluetoothGatt gatt) {
        try {
            Log.d("refreshDevice", "Called");
            Method localMethod = gatt.getClass().getMethod("refresh");
            if (localMethod != null) {
                boolean bool = ((Boolean) localMethod.invoke(gatt, new Object[0])).booleanValue();
                Log.d("refreshDevice", "bool: " + bool);
                return bool;
            }
        } catch (Exception localException) {
            Log.e("refreshDevice", "An exception occured while refreshing device");
        }
        return false;
    }

    public boolean isGattConnected() {
        return bluetoothGatt != null && bluetoothManager.getConnectionState(bluetoothGatt.getDevice(), BluetoothProfile.GATT) == BluetoothProfile.STATE_CONNECTED;
    }

    public boolean isGattConnected(String deviceAddress) {
        List<BluetoothDevice> list = bluetoothManager.getConnectedDevices(BluetoothProfile.GATT);
        for (BluetoothDevice bd : list) {
            if (deviceAddress != null && bd.getAddress().contains(deviceAddress)) {
                return true;
            }
        }
        return false;
    }

    public BluetoothGatt getConnectedGatt() {
        return bluetoothGatt;
    }

    public BluetoothGatt getConnectedGatt(String deviceAddress) {
        if (gattMap.get(deviceAddress) != null) {
            bluetoothGatt = gattMap.get(deviceAddress);
        }
        return bluetoothGatt;
    }

    public boolean writeGattCharacteristic(BluetoothGattCharacteristic characteristic) {
        if (bluetoothGatt != null) {
            return bluetoothGatt.writeCharacteristic(characteristic);
        }
        return false;
    }

    public boolean connectGatt(final BluetoothDevice device, boolean requestRssiUpdates, @Nullable final TimeoutGattCallback callback) {
        stopDiscovery();

        List<BluetoothDevice> devices = bluetoothManager.getConnectedDevices(BluetoothProfile.GATT);
        if (callback != null) {
            extraCallback = callback;
        }
        if (devices.contains(device)) {
            if (bluetoothGatt != null) {
                if (bluetoothGatt.getDevice().equals(device)) {
                    if (requestRssiUpdates) {
                        handler.post(rssiUpdate);
                    } else {
                        handler.removeCallbacks(rssiUpdate);
                    }
                }
            }
            return true;
        }
//        clearGatt();

        BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
            @Override
            public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
                Log.d(TAG, "onConnectionStateChange: ");
                Constants.LOGS.add(new ConnectionStateChangeLog(gatt, status, newState));
                super.onConnectionStateChange(gatt, status, newState);
                gattMap.put(device.getAddress(), gatt);
                handler.removeCallbacks(connectionTimeout);
                if (callback != null && newState == BluetoothGatt.STATE_DISCONNECTED) {
                    callback.onConnectionStateChange(gatt, status, newState);
                }
                if (extraCallback != null && (!(extraCallback.toString().contains("Browser") && newState == BluetoothGatt.STATE_DISCONNECTED))) {
                    extraCallback.onConnectionStateChange(gatt, status, newState);
                }

                if (newState == BluetoothProfile.STATE_DISCONNECTED) {

                    try {
                        gatt.close();
                    } catch (Exception e) {
                        Log.d(TAG, "close ignoring: " + e);
                    }
                }
            }

            @Override
            public void onServicesDiscovered(BluetoothGatt gatt, int status) {
                Log.d(TAG, "onServicesDiscovered: ");
                Constants.LOGS.add(new ServicesDiscoveredLog(gatt, status));
                super.onServicesDiscovered(gatt, status);
                if (extraCallback != null) {
                    extraCallback.onServicesDiscovered(gatt, status);
                }
            }

            @Override
            public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
                Log.d(TAG, "onCharacteristicWrite: " + characteristic);
                Constants.LOGS.add(new CommonLog("onCharacteristicWrite, " + "device: " + gatt.getDevice().getAddress() + ", status: " + status, gatt.getDevice().getAddress()));
                super.onCharacteristicWrite(gatt, characteristic, status);
                if (extraCallback != null) {
                    extraCallback.onCharacteristicWrite(gatt, characteristic, status);
                }
            }

            @Override
            public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
                Log.d(TAG, "onDescriptorRead: ");
                Constants.LOGS.add(new CommonLog("onDescriptorRead, " + "device: " + gatt.getDevice().getAddress() + ", status: " + status, gatt.getDevice().getAddress()));
                super.onDescriptorRead(gatt, descriptor, status);
                if (extraCallback != null) {
                    extraCallback.onDescriptorRead(gatt, descriptor, status);
                }
            }

            @Override
            public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
                Log.d(TAG, "onDescriptorWrite: ");
                Constants.LOGS.add(new CommonLog("onDescriptorWrite, " + "device: " + gatt.getDevice().getAddress() + ", status: " + status, gatt.getDevice().getAddress()));
                super.onDescriptorWrite(gatt, descriptor, status);
                if (extraCallback != null) {
                    extraCallback.onDescriptorWrite(gatt, descriptor, status);
                }
            }

            @Override
            public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
                Log.d(TAG, "onReliableWriteCompleted: ");
                Constants.LOGS.add(new CommonLog("onReliableWriteCompleted, " + "device: " + gatt.getDevice().getAddress() + ", status: " + status, gatt.getDevice().getAddress()));
                super.onReliableWriteCompleted(gatt, status);
                if (extraCallback != null) {
                    extraCallback.onReliableWriteCompleted(gatt, status);
                }
            }

            @Override
            public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
                Log.d(TAG, "onReadRemoteRssi: ");
                Constants.LOGS.add(new CommonLog("onReadRemoteRssi, " + "device: " + gatt.getDevice().getAddress() + ", status: " + status + ", rssi: " + rssi, gatt.getDevice().getAddress()));
                super.onReadRemoteRssi(gatt, rssi, status);
                if (extraCallback != null) {
                    extraCallback.onReadRemoteRssi(gatt, rssi, status);
                }
            }

            @Override
            public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
                Log.d(TAG, "onMtuChanged: ");
                Constants.LOGS.add(new CommonLog("onMtuChanged, " + "device: " + gatt.getDevice().getAddress() + ", status: " + status + ", mtu: " + mtu, gatt.getDevice().getAddress()));
                super.onMtuChanged(gatt, mtu, status);
                if (extraCallback != null) {
                    extraCallback.onMtuChanged(gatt, mtu, status);
                }
            }

            @Override
            public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
                super.onCharacteristicRead(gatt, characteristic, status);
                Log.d(TAG, "onCharacteristicRead: " + gatt.getDevice().getAddress() + status);//todo charact value ?
                Constants.LOGS.add(new CommonLog("onCharacteristicRead, " + "device: " + gatt.getDevice().getAddress() + ", status: " + status, gatt.getDevice().getAddress()));
                if (extraCallback != null) {
                    extraCallback.onCharacteristicRead(gatt, characteristic, status);
                }
            }

            @Override
            public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
                Log.d(TAG, "onCharacteristicChanged: " + gatt.getDevice().getAddress() + characteristic.getUuid().toString() + characteristic.getValue());//todo charact value ?
                Constants.LOGS.add(new CommonLog("onCharacteristicChanged, " + "device: " + gatt.getDevice().getAddress(), gatt.getDevice().getAddress()));
                super.onCharacteristicChanged(gatt, characteristic);
                if (extraCallback != null) {
                    extraCallback.onCharacteristicChanged(gatt, characteristic);
                }
            }
        };

        if (useBLE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            bluetoothGatt = device.connectGatt(this, false, gattCallback, BluetoothDevice.TRANSPORT_LE);
        } else {
            bluetoothGatt = device.connectGatt(this, false, gattCallback);
        }

        handler.postDelayed(connectionTimeout, CONNECTION_TIMEOUT);

        if (bluetoothGatt != null) {
            if (requestRssiUpdates) {
                handler.post(rssiUpdate);
            }
            return true;
        }
        return false;
    }

    private static class Listeners extends ArrayList<Listener> implements Listener {

        List<?> getScanFilterL() {

            List<ScanFilterCompat> scanFiltersCompat = getScanFilters();
            List<ScanFilter> scanFilters = (scanFiltersCompat != null) ? new ArrayList<ScanFilter>(scanFiltersCompat.size()) : null;
            if (scanFiltersCompat != null) {
                for (ScanFilterCompat scanFilterCompat : scanFiltersCompat) {
                    scanFilters.add(scanFilterCompat.createScanFilter());
                }
                return scanFilters.isEmpty() ? null : scanFilters;
            } else {
                return null;
            }
        }

        UUID[] getScanUuids() {
            List<ScanFilterCompat> scanFiltersCompat = getScanFilters();
            if ((scanFiltersCompat == null) || scanFiltersCompat.isEmpty()) {
                return null;
            }

            Set<UUID> uuids = new HashSet<>();
            for (ScanFilterCompat scanFilterCompat : scanFiltersCompat) {
                ParcelUuid serviceUuid = scanFilterCompat.getServiceUuid();
                if (serviceUuid != null) {
                    uuids.add(serviceUuid.getUuid());
                }
            }

            if (uuids.isEmpty()) {
                return null;
            }

            UUID[] retVal = new UUID[uuids.size()];
            return uuids.toArray(retVal);
        }

        @Override
        public List<ScanFilterCompat> getScanFilters() {
            List<ScanFilterCompat> result = new ArrayList<>();
            for (Listener listener : this) {
                List<ScanFilterCompat> scanFilters = listener.getScanFilters();
                if (scanFilters != null) {
                    for (ScanFilterCompat scanFilter : scanFilters) {
                        if (!result.contains(scanFilter)) {
                            result.add(scanFilter);
                        }
                    }
                }
            }
            return result.isEmpty() ? null : result;
        }

        @Override
        public boolean askForEnablingBluetoothAdapter() {
            for (Listener listener : this) {
                if (listener.askForEnablingBluetoothAdapter()) {
                    return true;
                }
            }
            return false;
        }

        @Override
        public void onStateChanged(int bluetoothAdapterState) {
            for (Listener listener : this) {
                listener.onStateChanged(bluetoothAdapterState);
            }
        }

        @Override
        public void onScanStarted() {
            for (Listener listener : this) {
                listener.onScanStarted();
            }
        }

        @Override
        public void onScanResultUpdated(List<BluetoothDeviceInfo> devices, BluetoothDeviceInfo changedDeviceInfo) {
            for (Listener listener : this) {
                listener.onScanResultUpdated(devices, changedDeviceInfo);
            }
        }

        @Override
        public void onScanEnded() {
            for (Listener listener : this) {
                listener.onScanEnded();
            }
        }

        @Override
        public void onDeviceReady(BluetoothDevice device, boolean isInteresting) {
            for (Listener listener : this) {
                listener.onDeviceReady(device, isInteresting);
            }
        }

        @Override
        public void onCharacteristicChanged(GattCharacteristic characteristic, Object value) {
            for (Listener listener : this) {
                listener.onCharacteristicChanged(characteristic, value);
            }
        }
    }
}