package com.silabs.thunderboard.ble;

import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.TaskStackBuilder;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.content.Intent;
import android.os.CountDownTimer;
import android.os.Handler;
import android.os.Parcelable;
import android.os.SystemClock;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
import android.widget.Toast;

import com.silabs.thunderboard.R;
import com.silabs.thunderboard.ble.model.ThunderBoardDevice;
import com.silabs.thunderboard.ble.model.ThunderBoardUuids;
import com.silabs.thunderboard.ble.util.BleUtils;
import com.silabs.thunderboard.common.app.ThunderBoardConstants;
import com.silabs.thunderboard.common.app.ThunderBoardType;
import com.silabs.thunderboard.common.data.PreferenceManager;
import com.silabs.thunderboard.common.data.model.ThunderBoardPreferences;
import com.silabs.thunderboard.common.injection.qualifier.ForApplication;
import com.silabs.thunderboard.demos.model.EnvironmentEvent;
import com.silabs.thunderboard.demos.model.HallState;
import com.silabs.thunderboard.demos.model.LedRGBState;
import com.silabs.thunderboard.demos.model.MotionEvent;
import com.silabs.thunderboard.demos.model.NotificationEvent;
import com.silabs.thunderboard.demos.model.StatusEvent;
import com.silabs.thunderboard.demos.ui.DemosSelectionActivity;

import org.altbeacon.beacon.Beacon;
import org.altbeacon.beacon.BeaconManager;
import org.altbeacon.beacon.BeaconParser;
import org.altbeacon.beacon.Identifier;
import org.altbeacon.beacon.RangeNotifier;
import org.altbeacon.beacon.Region;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import javax.inject.Inject;
import javax.inject.Singleton;

import rx.Observable;
import rx.Subscriber;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
import rx.subjects.BehaviorSubject;
import rx.subjects.PublishSubject;
import timber.log.Timber;

import static com.silabs.thunderboard.ble.model.ThunderBoardUuids.UUID_CHARACTERISTIC_AMBIENT_LIGHT_REACT;
import static com.silabs.thunderboard.ble.model.ThunderBoardUuids.UUID_CHARACTERISTIC_CO2_READING;
import static com.silabs.thunderboard.ble.model.ThunderBoardUuids.UUID_CHARACTERISTIC_HALL_FIELD_STRENGTH;
import static com.silabs.thunderboard.ble.model.ThunderBoardUuids.UUID_CHARACTERISTIC_HALL_STATE;
import static com.silabs.thunderboard.ble.model.ThunderBoardUuids.UUID_CHARACTERISTIC_HUMIDITY;
import static com.silabs.thunderboard.ble.model.ThunderBoardUuids.UUID_CHARACTERISTIC_PRESSURE;
import static com.silabs.thunderboard.ble.model.ThunderBoardUuids.UUID_CHARACTERISTIC_SOUND_LEVEL;
import static com.silabs.thunderboard.ble.model.ThunderBoardUuids.UUID_CHARACTERISTIC_TVOC_READING;
import static com.silabs.thunderboard.ble.model.ThunderBoardUuids.UUID_CHARACTERISTIC_UV_INDEX;
import static com.silabs.thunderboard.ble.model.ThunderBoardUuids.UUID_SERVICE_AMBIENT_LIGHT;
import static com.silabs.thunderboard.ble.model.ThunderBoardUuids.UUID_SERVICE_ENVIRONMENT_SENSING;
import static com.silabs.thunderboard.ble.model.ThunderBoardUuids.UUID_SERVICE_HALL_EFFECT;
import static com.silabs.thunderboard.ble.model.ThunderBoardUuids.UUID_SERVICE_INDOOR_AIR_QUALITY;

/**
 * Provides methods for the application to interact with the Android BLE system.
 */
@Singleton
public class BleManager implements RangeNotifier {

    private final static long EXPIRATION_TIMER_INTERVAL = 1000;
    private final static long MAX_AGE = 10000;

    public final PublishSubject<List<ThunderBoardDevice>> scanner = PublishSubject.create();
    public final BehaviorSubject<ThunderBoardDevice> selectedDeviceMonitor = BehaviorSubject.create();
    public final BehaviorSubject<StatusEvent> selectedDeviceStatusMonitor = BehaviorSubject.create();
    public final PublishSubject<MotionEvent> motionDetector = PublishSubject.create();
    public final PublishSubject<EnvironmentEvent> environmentDetector = PublishSubject.create();
    public final PublishSubject<EnvironmentEvent> environmentReadMonitor = PublishSubject.create();
    public final BehaviorSubject<NotificationEvent> notificationsMonitor = BehaviorSubject.create();

    private final Context context;
    private final PreferenceManager preferenceManager;
    private final BluetoothManager bluetoothManager;
    private final BluetoothAdapter bluetoothAdapter;

    private final List<ThunderBoardDevice> devices = new ArrayList<>();
    private final List<ThunderBoardDevice> activeDevices = new ArrayList<>();
    private final Map<String, Long> deviceAgeMap = new HashMap<>();

    private final BeaconManager beaconManager;
    private final GattManager gattManager;

    // Should be cleared/null when connection sessions begins
    private BluetoothGatt gatt;
    private Subscriber<NotificationEvent> configureMotionSubscriber;
    private Subscriber<NotificationEvent> configureEnvironmentSubscriber;
    private Subscriber<ThunderBoardDevice> configureIOSubscriber;

    private Subscriber<Long> intervalSubscriber;

    public boolean characteristicHallStateAvailable;
    public boolean characteristicHallFieldStrengthAvailable;
    public boolean characteristicCo2ReadingAvailable;
    public boolean characteristicTvocReadingAvailable;
    public boolean characteristicPressureAvailable;
    public boolean characteristicSoundLevelAvailable;
    public boolean characteristicHumidityAvailable;
    public boolean characteristicUvIndexAvailable;
    public boolean characteristicAmbientLightReactAvailable;
    public boolean characteristicAmbientLightSenseAvailable;

    private LedRGBState ledRGBState;

    private Handler handler;

    private Runnable retryWriteLed = new Runnable() {
        @Override
        public void run() {
            handler.removeCallbacks(retryWriteLed);
            setColorLEDs(ledRGBState);
        }
    };

    @Inject
    public BleManager(@ForApplication Context context, PreferenceManager prefsManager) {
        this.context = context;

        // the app manifest requires support for BLE, no need to check explicitly
        bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
        bluetoothAdapter = bluetoothManager.getAdapter();
        preferenceManager = prefsManager;
        gattManager = new GattManager(prefsManager, this);

        // Beaconing
        beaconManager = org.altbeacon.beacon.BeaconManager.getInstanceForApplication(context);
        beaconManager.getBeaconParsers().clear();
        beaconManager.getBeaconParsers().add(new BeaconParser().
                setBeaconLayout("m:2-3=0215,i:4-19,i:20-21,i:22-23,p:24-24,d:25-25"));

        Timber.d("setting up background monitoring for beacons and power saving");
        Identifier id1 = Identifier.parse(ThunderBoardDevice.THUNDER_BOARD_REACT_UUID_STRING);
        Region region = new Region("backgroundRegion", id1, null, null);
        regionBootstrap = new ThunderBoardBootstrap(context, this, region);
        backgroundPowerSaver = new ThunderBoardPowerSaver(context, preferenceManager);

        beaconManager.setBackgroundBetweenScanPeriod(ThunderBoardPowerSaver.DELAY_BETWEEN_SCANS_INACTIVE);

        handler = new Handler();
    }

    public boolean isBluetoothEnabled() {
        return bluetoothAdapter.isEnabled();
    }

    public void foregroundScan() {
        clearDevices();
        devicesNotFoundMonitor.start();
        Timber.d("has observers %s", scanner.hasObservers());
    }

    public void backgroundScan() {
        Timber.d("has observers %s gatt is null: %s", scanner.hasObservers(), (gatt == null));
        devicesNotFoundMonitor.cancel();
    }

    public void clearDevices() {
        Timber.d("all");
        closeGatt();
        // this will give a chance to complete the timer w/o triggering no devices
        activeDevices.clear();
        deviceAgeMap.clear();
        devices.clear();
    }

    public void connect(String deviceAddress) {

        Timber.d("%s", deviceAddress);

        if (gatt != null) {
            Timber.d("gat not null, closing and reconnecting");
            closeGatt();
            connect(deviceAddress);
            return;
        }

        final BluetoothDevice device = bluetoothAdapter.getRemoteDevice(deviceAddress);

        if (device == null) {
            throw new IllegalStateException("Connecting to a non discovered device is not supported.");
        } else {
            // This is where the connection session starts
            gatt = device.connectGatt(context, false, gattManager.gattCallbacks);
            ThunderBoardDevice tbd = getDeviceFromCache(deviceAddress);
            if (tbd == null) {
                tbd = new ThunderBoardDevice(device, 0);
                devices.add(tbd);
                deviceAgeMap.put(tbd.getAddress(), System.currentTimeMillis());
            }
            tbd.setState(BluetoothProfile.STATE_CONNECTING);
            selectedDeviceMonitor.onNext(tbd);
            selectedDeviceStatusMonitor.onNext(new StatusEvent(tbd));
            return;
        }
    }

    // Demo Configuration interfaces
    public void configureIO() {
        if (gatt != null) {
            final ThunderBoardDevice device = getDeviceFromCache(gatt.getDevice().getAddress());
            if (device != null) {
                Timber.d("configure sensor for: %s", device.getAddress());
                ThunderBoardSensorIo sensor = new ThunderBoardSensorIo();
                sensor.isNotificationEnabled = false;
                device.setSensorIo(sensor);

                unsubscribeConfigureIOSubscriber();
                this.configureIOSubscriber = enableConfigureIO();
                this.selectedDeviceMonitor
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribeOn(Schedulers.io())
                        // wait a second before subscribing to notification
                        .delay(1000, TimeUnit.MILLISECONDS)
                        .subscribe(this.configureIOSubscriber);
                configureIOSettings();
            }
        }
    }

    private void configureIOSettings() {
        BleUtils.readCharacteristic(gatt, ThunderBoardUuids.UUID_SERVICE_AUTOMATION_IO, ThunderBoardUuids.UUID_CHARACTERISTIC_DIGITAL);
    }

    private Subscriber<ThunderBoardDevice> enableConfigureIO() {
        return new Subscriber<ThunderBoardDevice>() {
            @Override
            public void onCompleted() {

            }

            @Override
            public void onError(Throwable e) {

            }

            @Override
            public void onNext(ThunderBoardDevice device) {

                boolean submitted =
                        BleUtils.setCharacteristicNotification(gatt,
                                ThunderBoardUuids.UUID_SERVICE_AUTOMATION_IO,
                                ThunderBoardUuids.UUID_CHARACTERISTIC_DIGITAL,
                                ThunderBoardUuids
                                        .UUID_DESCRIPTOR_CLIENT_CHARACTERISTIC_CONFIGURATION,
                                true);
                if (!submitted) {
                    ThunderBoardSensorIo sensor = device.getSensorIo();
                    if (sensor != null) {
                        sensor.isNotificationEnabled = false;
                        Toast.makeText(context, R.string.iodemo_alert_configuration_failed,
                                Toast.LENGTH_SHORT).show();
                    }
                }

                unsubscribeConfigureIOSubscriber();
            }
        };
    }

    private void unsubscribeConfigureIOSubscriber() {
        if (configureIOSubscriber != null && !configureIOSubscriber.isUnsubscribed()) {
            configureIOSubscriber.unsubscribe();
        }
        configureIOSubscriber = null;
    }

    // Demo IO actions
    public void ledAction(int ledSent) {
        boolean submitted = BleUtils.writeCharacteristics(
                gatt,
                ThunderBoardUuids.UUID_SERVICE_AUTOMATION_IO,
                ThunderBoardUuids.UUID_CHARACTERISTIC_DIGITAL,
                ledSent, BluetoothGattCharacteristic.FORMAT_UINT8,
                0
        );
        if (!submitted) {
            Timber.i(context.getString(R.string.iodemo_alert_action_failed));
        }
        Timber.d("write led  %02x submitted: %s", ledSent, submitted);
    }

    private void configureMotionCalibrate(boolean enabled) {
        boolean submitted = BleUtils.setCharacteristicIndications(
                gatt,
                ThunderBoardUuids.UUID_SERVICE_ACCELERATION_ORIENTATION,
                ThunderBoardUuids.UUID_CHARACTERISTIC_CALIBRATE,
                ThunderBoardUuids.UUID_DESCRIPTOR_CLIENT_CHARACTERISTIC_CONFIGURATION, enabled);
        Timber.d("%s acceleration indication submitted: %s", enabled, submitted);
    }

    private Subscriber<NotificationEvent> enableConfigureMotion(final ThunderBoardDevice device) {
        return new Subscriber<NotificationEvent>() {
            @Override
            public void onCompleted() {

            }

            @Override
            public void onError(Throwable e) {

            }

            @Override
            public void onNext(NotificationEvent notificationEvent) {
                if (device.isCalibrateNotificationEnabled == null || !device.isCalibrateNotificationEnabled) {
                    configureMotionCalibrate(true);
                    return;
                }

                if (device.isAccelerationNotificationEnabled == null || !device.isAccelerationNotificationEnabled) {
                    boolean submitted = enableAcceleration(true);
                    Timber.d("enable acceleration indication submitted: %s", submitted);
                    return;
                }

                if (device.isOrientationNotificationEnabled == null || !device.isOrientationNotificationEnabled) {
                    boolean submitted = enableOrientation(true);
                    Timber.d("enable orientation indication submitted: %s", submitted);
                    return;
                }

                if (device.isRotationNotificationEnabled == null || !device.isRotationNotificationEnabled) {
                    boolean submitted = enableCscMeasurement(true);
                    Timber.d("enable rotation notification submitted: %s", submitted);
                    return;
                }

                if (getThunderBoardType() == ThunderBoardType.THUNDERBOARD_SENSE) {
                    BleManager.this.readColorLEDs();
                }

                unsubscribeConfigureMotionSubscriber();
            }
        };
    }

    public void configureMotion() {
        if (gatt != null) {
            final ThunderBoardDevice device = getDeviceFromCache(gatt.getDevice().getAddress());
            if (device != null) {
                Timber.d("configure sensor for: %s", device.getAddress());
                float wheelRadius = (preferenceManager.getPreferences().wheelRadius == 0) ? ThunderBoardPreferences.DEFAULT_WHEEL_RADIUS : preferenceManager.getPreferences().wheelRadius;
                ThunderBoardSensorMotion sensor = new ThunderBoardSensorMotion(preferenceManager.getPreferences().measureUnitType, wheelRadius);
                sensor.isNotificationEnabled = false;
                device.setSensorMotion(sensor);

                unsubscribeConfigureMotionSubscriber();
                this.configureMotionSubscriber = enableConfigureMotion(device);
                this.notificationsMonitor
                        .delay(500, TimeUnit.MILLISECONDS)
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribeOn(Schedulers.io())
                        .subscribe(this.configureMotionSubscriber);
                configureMotionCalibrate(true);
            }
        }
    }

    private void unsubscribeConfigureMotionSubscriber() {
        if (configureMotionSubscriber != null && !configureMotionSubscriber.isUnsubscribed()) {
            configureMotionSubscriber.unsubscribe();
        }
        configureMotionSubscriber = null;
    }

    private void clearCalibrateNotification() {
        if (gatt != null) {
            boolean submittedCalibrate = BleUtils.unsetCharacteristicNotification(
                    gatt,
                    ThunderBoardUuids.UUID_SERVICE_ACCELERATION_ORIENTATION,
                    ThunderBoardUuids.UUID_CHARACTERISTIC_CALIBRATE,
                    ThunderBoardUuids.UUID_DESCRIPTOR_CLIENT_CHARACTERISTIC_CONFIGURATION, false);
            Timber.d("disable calibration indication submitted: %s", submittedCalibrate);
        }
    }

    private void clearAccelerationNotification() {
        if (gatt != null) {
            boolean submittedAcceleration = BleUtils.unsetCharacteristicNotification(
                    gatt,
                    ThunderBoardUuids.UUID_SERVICE_ACCELERATION_ORIENTATION,
                    ThunderBoardUuids.UUID_CHARACTERISTIC_ACCELERATION,
                    ThunderBoardUuids.UUID_DESCRIPTOR_CLIENT_CHARACTERISTIC_CONFIGURATION, false);
            Timber.d("disable acceleration indication submitted: %s", submittedAcceleration);
        }
    }

    private void clearOrientationNotification() {
        if (gatt != null) {
            boolean submittedOrientation = BleUtils.unsetCharacteristicNotification(
                    gatt,
                    ThunderBoardUuids.UUID_SERVICE_ACCELERATION_ORIENTATION,
                    ThunderBoardUuids.UUID_CHARACTERISTIC_ORIENTATION,
                    ThunderBoardUuids.UUID_DESCRIPTOR_CLIENT_CHARACTERISTIC_CONFIGURATION, false);
            Timber.d("disable orientation indication submitted: %s", submittedOrientation);
        }
    }

    private void clearRotationNotification() {
        enableCscMeasurement(false);
    }


    public void clearMotionNotifications() {
        handleClearMotionNotifications(null);
    }

    public void clearHallStateNotifications() {
        if (gatt != null) {
            boolean submitted = BleUtils.unsetCharacteristicNotification(
                    gatt,
                    UUID_SERVICE_HALL_EFFECT,
                    ThunderBoardUuids.UUID_CHARACTERISTIC_HALL_STATE,
                    ThunderBoardUuids.UUID_DESCRIPTOR_CLIENT_CHARACTERISTIC_CONFIGURATION, false);
            Timber.d("disable hall state submitted: %s", submitted);
        }
    }

    /**
     * @param notificationEvent
     * @return returns true if clear is in progress, false otherwise
     */
    public boolean handleClearMotionNotifications(@Nullable NotificationEvent notificationEvent) {
        if (gatt != null) {
            if (notificationEvent == null) {
                clearCalibrateNotification();
                return true;
            }

            // hacky way of chaining order of operations together :-(
            // TODO REFACTOR
            if (NotificationEvent.ACTION_NOTIFICATIONS_CLEAR == notificationEvent.action) {
                if (ThunderBoardUuids.UUID_CHARACTERISTIC_CALIBRATE.equals(
                        notificationEvent.characteristicUuid)) {
                    clearAccelerationNotification();
                    return true;
                }
                if (ThunderBoardUuids.UUID_CHARACTERISTIC_ACCELERATION.equals(
                        notificationEvent.characteristicUuid)) {
                    clearOrientationNotification();
                    return true;
                }

                if (ThunderBoardUuids.UUID_CHARACTERISTIC_ORIENTATION.equals(
                        notificationEvent.characteristicUuid)) {
                    clearRotationNotification();
                    return true;
                }
            }
        }
        return false;
    }

    public boolean readCscFeature() {
        return BleUtils.readCharacteristic(gatt, ThunderBoardUuids.UUID_SERVICE_CSC, ThunderBoardUuids.UUID_CHARACTERISTIC_CSC_FEATURE);
    }

    public boolean enableOrientation(boolean enabled) {
        return BleUtils.setCharacteristicNotification(
                gatt,
                ThunderBoardUuids.UUID_SERVICE_ACCELERATION_ORIENTATION,
                ThunderBoardUuids.UUID_CHARACTERISTIC_ORIENTATION,
                ThunderBoardUuids.UUID_DESCRIPTOR_CLIENT_CHARACTERISTIC_CONFIGURATION, enabled);
    }

    public boolean enableAcceleration(boolean enabled) {
        return BleUtils.setCharacteristicNotification(
                gatt,
                ThunderBoardUuids.UUID_SERVICE_ACCELERATION_ORIENTATION,
                ThunderBoardUuids.UUID_CHARACTERISTIC_ACCELERATION,
                ThunderBoardUuids.UUID_DESCRIPTOR_CLIENT_CHARACTERISTIC_CONFIGURATION, enabled);
    }

    public boolean enableCscMeasurement(boolean enabled) {
        return BleUtils.setCharacteristicNotification(
                gatt,
                ThunderBoardUuids.UUID_SERVICE_CSC,
                ThunderBoardUuids.UUID_CHARACTERISTIC_CSC_MEASUREMENT,
                ThunderBoardUuids.UUID_DESCRIPTOR_CLIENT_CHARACTERISTIC_CONFIGURATION, enabled);
    }

    public boolean enableHallStateMeasurement(boolean enabled) {
        return BleUtils.setCharacteristicNotification(
                gatt,
                UUID_SERVICE_HALL_EFFECT,
                ThunderBoardUuids.UUID_CHARACTERISTIC_HALL_STATE,
                ThunderBoardUuids.UUID_DESCRIPTOR_CLIENT_CHARACTERISTIC_CONFIGURATION, enabled);
    }

    public boolean startCalibration() {
        boolean submitted = BleUtils.writeCharacteristics(gatt, ThunderBoardUuids
                .UUID_SERVICE_ACCELERATION_ORIENTATION, ThunderBoardUuids
                .UUID_CHARACTERISTIC_CALIBRATE, 0x01, BluetoothGattCharacteristic.FORMAT_UINT8, 0);
        Timber.d("submitted: %s", submitted);
        return submitted;
    }

    public boolean resetOrientation() {
        boolean submitted = BleUtils.writeCharacteristics(gatt, ThunderBoardUuids
                .UUID_SERVICE_ACCELERATION_ORIENTATION, ThunderBoardUuids
                .UUID_CHARACTERISTIC_CALIBRATE, 0x02, BluetoothGattCharacteristic.FORMAT_UINT8, 0);
        Timber.d("submitted: %s", submitted);
        return submitted;
    }

    public boolean resetRevolutions() {
        boolean submitted = BleUtils.writeCharacteristics(gatt, ThunderBoardUuids
                .UUID_SERVICE_ACCELERATION_ORIENTATION, ThunderBoardUuids
                .UUID_CHARACTERISTIC_CSC_CONTROL_POINT, 0x01, BluetoothGattCharacteristic
                .FORMAT_UINT8, 0);
        Timber.d("submitted: %s", submitted);
        return submitted;
    }

    public boolean resetHallEffectTamper() {
        boolean submitted = BleUtils.writeCharacteristics(gatt,
                UUID_SERVICE_HALL_EFFECT,
                ThunderBoardUuids.UUID_CHARACTERISTIC_HALL_CONTROL_POINT,
                HallState.OPENED,
                BluetoothGattCharacteristic.FORMAT_UINT16,
                0);
        Timber.d("submitted: %s", submitted);
        return submitted;
    }

    public void setColorLEDs(LedRGBState ledRGBState) {
        this.ledRGBState = ledRGBState;
        handler.removeCallbacks(retryWriteLed);

        ThunderBoardDevice device = getDeviceFromCache(gatt.getDevice().getAddress());
        if (device != null && device.getSensorIo() != null && device.getSensorIo().getSensorData() != null) {
            device.getSensorIo().getSensorData().colorLed = null;
        }

        byte[] bytes = new byte[4];

        bytes[0] = (byte) (ledRGBState.color.blue & 0xff);
        bytes[1] = (byte) (ledRGBState.color.green & 0xff);
        bytes[2] = (byte) (ledRGBState.color.red & 0xff);
        bytes[3] = (byte) (ledRGBState.on ? 0x0f : 0x00);

        int value = ByteBuffer.wrap(bytes).getInt();

        boolean characteristicsWriteResult = BleUtils.writeCharacteristics(gatt,
                ThunderBoardUuids.UUID_SERVICE_USER_INTERFACE,
                ThunderBoardUuids.UUID_CHARACTERISTIC_RGB_LEDS,
                value,
                BluetoothGattCharacteristic.FORMAT_UINT32, 0);

        if (!characteristicsWriteResult) {
            handler.postDelayed(retryWriteLed, 100);
        }
    }

    private Subscriber<NotificationEvent> enableConfigureEnvironment(final ThunderBoardDevice device) {
        return new Subscriber<NotificationEvent>() {
            @Override
            public void onCompleted() {

            }

            @Override
            public void onError(Throwable e) {
                Timber.d(e.getMessage());
            }

            @Override
            public void onNext(NotificationEvent notificationEvent) {
                if (device.isHallStateNotificationEnabled == null || !device.isHallStateNotificationEnabled) {
                    boolean submitted = enableHallStateMeasurement(true);
                    Timber.d("enable hall state notification submitted: %s", submitted);
                    return;
                }
                unsubscribeConfigureEnvironmentSubscriber();
            }
        };
    }

    public void configureEnvironment() {
        if (gatt != null) {
            ThunderBoardDevice device = getDeviceFromCache(gatt.getDevice().getAddress());
            if (device != null) {
                ThunderBoardSensorEnvironment sensor = new ThunderBoardSensorEnvironment(preferenceManager.getPreferences().temperatureType);
                sensor.isNotificationEnabled = false;
                device.setSensorEnvironment(sensor);

                unsubscribeConfigureEnvironmentSubscriber();
                this.configureEnvironmentSubscriber = enableConfigureEnvironment(device);
                this.notificationsMonitor
                        .delay(500, TimeUnit.MILLISECONDS)
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribeOn(Schedulers.io())
                        .subscribe(this.configureEnvironmentSubscriber);
            }
        }
    }

    private void unsubscribeConfigureEnvironmentSubscriber() {
        if (configureEnvironmentSubscriber != null && !configureEnvironmentSubscriber.isUnsubscribed()) {
            configureEnvironmentSubscriber.unsubscribe();
        }
        configureEnvironmentSubscriber = null;
    }

    public boolean readTemperature() {
        return BleUtils.readCharacteristic(gatt, UUID_SERVICE_ENVIRONMENT_SENSING,
                ThunderBoardUuids.UUID_CHARACTERISTIC_TEMPERATURE);
    }

    public boolean readHumidity() {
        return BleUtils.readCharacteristic(gatt, UUID_SERVICE_ENVIRONMENT_SENSING,
                UUID_CHARACTERISTIC_HUMIDITY);
    }

    public boolean readUvIndex() {
        return BleUtils.readCharacteristic(gatt, UUID_SERVICE_ENVIRONMENT_SENSING,
                UUID_CHARACTERISTIC_UV_INDEX);
    }

    public boolean readAmbientLightReact() {
        return BleUtils.readCharacteristic(gatt, UUID_SERVICE_AMBIENT_LIGHT,
                UUID_CHARACTERISTIC_AMBIENT_LIGHT_REACT);
    }

    public boolean readAmbientLightSense() {
        return BleUtils.readCharacteristic(gatt, UUID_SERVICE_ENVIRONMENT_SENSING,
                UUID_CHARACTERISTIC_AMBIENT_LIGHT_REACT);
    }

    public boolean readSoundLevel() {
        return BleUtils.readCharacteristic(gatt, UUID_SERVICE_ENVIRONMENT_SENSING,
                UUID_CHARACTERISTIC_SOUND_LEVEL);
    }

    public boolean readPressure() {
        return BleUtils.readCharacteristic(gatt, UUID_SERVICE_ENVIRONMENT_SENSING,
                UUID_CHARACTERISTIC_PRESSURE);
    }

    public boolean readCO2Level() {
        return BleUtils.readCharacteristic(gatt, UUID_SERVICE_INDOOR_AIR_QUALITY,
                UUID_CHARACTERISTIC_CO2_READING);
    }

    public boolean readTVOCLevel() {
        return BleUtils.readCharacteristic(gatt, UUID_SERVICE_INDOOR_AIR_QUALITY,
                UUID_CHARACTERISTIC_TVOC_READING);
    }

    public boolean readColorLEDs() {
        return BleUtils.readCharacteristic(gatt, ThunderBoardUuids.UUID_SERVICE_USER_INTERFACE,
                ThunderBoardUuids.UUID_CHARACTERISTIC_RGB_LEDS);
    }

    public boolean readHallStrength() {
        return BleUtils.readCharacteristic(gatt, UUID_SERVICE_HALL_EFFECT,
                ThunderBoardUuids.UUID_CHARACTERISTIC_HALL_FIELD_STRENGTH);
    }

    public boolean readHallState() {
        return BleUtils.readCharacteristic(gatt, UUID_SERVICE_HALL_EFFECT,
                ThunderBoardUuids.UUID_CHARACTERISTIC_HALL_STATE);
    }


    public ThunderBoardType getThunderBoardType() {
        if (gatt == null) {
            return ThunderBoardType.UNKNOWN;
        }
        ThunderBoardDevice device = getDeviceFromCache(gatt.getDevice().getAddress());
        return device == null ? ThunderBoardType.UNKNOWN : device.getThunderBoardType();
    }

    private void closeGatt() {

        if (gatt != null) {

            Timber.d("gatt device: %s, connected devices: %d", gatt.getDevice().getAddress(), bluetoothManager.getConnectedDevices(BluetoothProfile.GATT).size());
            ThunderBoardDevice device = getDeviceFromCache(gatt.getDevice().getAddress());
            if (device != null) {
                device.clear();
            }

            if (BluetoothGatt.STATE_DISCONNECTED == bluetoothManager.getConnectionState(gatt.getDevice(), BluetoothProfile.GATT)) {
                Timber.d("close");
                gatt.close();
            } else {
                Timber.d("disconnect");
                gatt.disconnect();
            }

            gatt = null;
        }
        for (int i = 0; i < devices.size(); i++) {
            Timber.d("device: %s", devices.get(i).getAddress());
        }
    }

    ThunderBoardDevice getDeviceFromCache(String deviceAddress) {
        for (int i = 0; i < devices.size(); i++) {
            ThunderBoardDevice device = devices.get(i);
            if (device.getAddress().equals(deviceAddress)) {
                return device;
            }
        }
        return null;
    }

    void readRequiredCharacteristics() {
        if (gatt != null) {
            ThunderBoardDevice device = getDeviceFromCache(gatt.getDevice().getAddress());
            if (device != null) {
                boolean readSuccessful = false;
                if (device.isOriginalNameNull()) {
                    readSuccessful = BleUtils.readCharacteristic(gatt, ThunderBoardUuids.UUID_SERVICE_GENERIC_ACCESS, ThunderBoardUuids.UUID_CHARACTERISTIC_DEVICE_NAME);
                    Timber.d("read device name submitted: %s", readSuccessful);
                } else if (device.getModelNumber() == null) {
                    readSuccessful = BleUtils.readCharacteristic(gatt, ThunderBoardUuids.UUID_SERVICE_DEVICE_INFORMATION, ThunderBoardUuids.UUID_CHARACTERISTIC_MODEL_NUMBER);
                    Timber.d("read model number submitted: %s", readSuccessful);
                } else if (device.getSystemId() == null) {
                    readSuccessful = BleUtils.readCharacteristic(gatt, ThunderBoardUuids
                            .UUID_SERVICE_DEVICE_INFORMATION, ThunderBoardUuids.UUID_CHARACTERISTIC_SYSTEM_ID);
                    Timber.d("read system id submitted: %s", readSuccessful);
                } else if (device.isBatteryConfigured == null) {
                    readSuccessful = BleUtils.readCharacteristic(gatt, ThunderBoardUuids.UUID_SERVICE_BATTERY, ThunderBoardUuids.UUID_CHARACTERISTIC_BATTERY_LEVEL);
                    if (!readSuccessful) {
                        device.isBatteryConfigured = false;
                    }
                    Timber.d("read battery level submitted: %s", readSuccessful);
                } else if (device.isBatteryNotificationEnabled == null) {
                    readSuccessful = BleUtils.setCharacteristicNotification(gatt, ThunderBoardUuids.UUID_SERVICE_BATTERY, ThunderBoardUuids.UUID_CHARACTERISTIC_BATTERY_LEVEL, ThunderBoardUuids.UUID_DESCRIPTOR_CLIENT_CHARACTERISTIC_CONFIGURATION, true);
                    Timber.d("enable battery notification submitted: %s", readSuccessful);
                    if (!readSuccessful) {
                        device.isBatteryNotificationEnabled = false;
                    }
                } else if (device.isPowerSourceConfigured == null) {
                    readSuccessful = BleUtils.readCharacteristic(gatt, ThunderBoardUuids.UUID_SERVICE_POWER_MANAGEMENT, ThunderBoardUuids.UUID_CHARACTERISTIC_POWER_SOURCE);
                    if (!readSuccessful) {
                        device.isPowerSourceConfigured = false;
                    }
                    Timber.d("read power source submitted: %s", readSuccessful);
                } else if (device.isPowerSourceNotificationEnabled == null) {
                    readSuccessful = BleUtils.setCharacteristicNotification(gatt,
                            ThunderBoardUuids.UUID_SERVICE_POWER_MANAGEMENT,
                            ThunderBoardUuids.UUID_CHARACTERISTIC_POWER_SOURCE,
                            ThunderBoardUuids.UUID_DESCRIPTOR_CLIENT_CHARACTERISTIC_CONFIGURATION, true);
                    Timber.d("enable power source notification submitted: %s", readSuccessful);
                    if (!readSuccessful) {
                        device.isPowerSourceNotificationEnabled = false;
                    }
                } else if (device.getFirmwareVersion() == null) {
                    readSuccessful = BleUtils.readCharacteristic(gatt, ThunderBoardUuids.UUID_SERVICE_DEVICE_INFORMATION, ThunderBoardUuids.UUID_CHARACTERISTIC_FIRMWARE_REVISION);
                    Timber.d("read firmware submitted: %s", readSuccessful);
                } else {
                    // out of items to read
                    readSuccessful = true;
                }

                if (!readSuccessful) {
                    readRequiredCharacteristics();
                }
            }
        }
    }

    // Beaconing

    private ThunderBoardBootstrap regionBootstrap;
    private ThunderBoardPowerSaver backgroundPowerSaver;

    // RangeNotifier interface
    @Override
    public void didRangeBeaconsInRegion(Collection<Beacon> beacons, Region region) {

        Iterator<Beacon> iterator = beacons.iterator();

        while (iterator.hasNext()) {

            //EditText editText = (EditText)RangingActivity.this.findViewById(R.id.rangingText);
            Beacon beacon = iterator.next();
            Timber.d("beacon %s  is about: %f meters away.", beacon.toString(), beacon.getDistance());

            deviceFound(beacon);
            if (backgroundPowerSaver.isScannerActivityResumed()) {
                publishRecentBeacons();
            } else if (SystemClock.elapsedRealtime() - backgroundPowerSaver.getScannerActivityDestroyedTimestamp() >
                    ThunderBoardPowerSaver.DELAY_NOTIFICATIONS_TIME_THRESHOLD
                    && backgroundPowerSaver.isApplicationBackgrounded()) {
                Timber.d("Sending notification.");
                sendNotification(beacon);
            }
        }
    }


    private void deviceFound(Beacon beacon) {
        ThunderBoardDevice old = getDeviceFromCache(beacon.getBluetoothAddress());
        if (old != null) {
            Timber.d("rssi: %d, has observers: %s, old state: %s", beacon.getRssi(), scanner.hasObservers(), old.getState());
            old.setRssi(beacon.getRssi());
            deviceAgeMap.put(old.getAddress(), System.currentTimeMillis());
        } else {
            Timber.d("appended, rssi: %d, has observers: %s", beacon.getRssi(), scanner.hasObservers());
            ThunderBoardDevice bleDevice = new ThunderBoardDevice(beacon);
            devices.add(bleDevice);
            deviceAgeMap.put(bleDevice.getAddress(), System.currentTimeMillis());
        }
    }

    public void addNotificationDevice(ThunderBoardDevice notificationDevice) {
        ThunderBoardDevice old = getDeviceFromCache(notificationDevice.getAddress());
        if (old == null) {
            devices.add(notificationDevice);
        }
        deviceAgeMap.put(notificationDevice.getAddress(), System.currentTimeMillis());
    }

    private void sendNotification(Beacon beacon) {

        // check if notifications are enabled and allowed for the beacon
        if (!BleUtils.checkAllowNotifications(beacon.getBluetoothAddress(), preferenceManager.getPreferences())) {
            Timber.d("Notifications not allowed for : %s, address: %s", beacon.getBluetoothName(), beacon.getBluetoothAddress());
            return;
        }

        NotificationCompat.Builder builder =
                new NotificationCompat.Builder(context)
                        .setContentTitle(context.getResources().getString(R.string.app_name))
                        .setContentText(String.format("%s is nearby.", beacon.getBluetoothName()))
                        .setSmallIcon(R.drawable.ic_stat_sl_beacon)
                        .setAutoCancel(true);


        TaskStackBuilder stackBuilder = TaskStackBuilder.create(context);

        Intent intent = new Intent(context, DemosSelectionActivity.class);
        intent.putExtra(ThunderBoardConstants.EXTRA_DEVICE_BEACON, (Parcelable) beacon);
        stackBuilder.addParentStack(DemosSelectionActivity.class);
        stackBuilder.addNextIntent(intent);

        PendingIntent resultPendingIntent =
                stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
        builder.setContentIntent(resultPendingIntent);
        NotificationManager notificationManager =
                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
        notificationManager.notify(beacon.getId3().toInt(), builder.build());
    }

    private final CountDownTimer devicesNotFoundMonitor = new CountDownTimer(10000, 2000) {

        @Override
        public void onTick(long millisUntilFinished) {
            Timber.d("devices: %d", devices.size());
            if (devices.size() > 0) {
                this.cancel();
            }
        }

        @Override
        public void onFinish() {
            if (devices.size() == 0) {
                publishRecentBeacons();
            }
        }
    };

    private Subscriber<Long> onInterval() {
        return new Subscriber<Long>() {
            @Override
            public void onCompleted() {
                Timber.d("interval subscriber completed");
                if (!isUnsubscribed()) {
                    unsubscribe();
                }
            }

            @Override
            public void onError(Throwable e) {
                Timber.e("interval subscriber error: %s", e.getMessage());
                if (!isUnsubscribed()) {
                    unsubscribe();
                }
            }

            @Override
            public void onNext(Long aLong) {
                publishRecentBeacons();
            }
        };
    }

    public void unsubscribeInterval() {
        if (intervalSubscriber != null && !intervalSubscriber.isUnsubscribed()) {
            intervalSubscriber.unsubscribe();
        }
        intervalSubscriber = null;
    }

    public void subscribeInterval() {
        intervalSubscriber = onInterval();
        Observable.interval(EXPIRATION_TIMER_INTERVAL, TimeUnit.MILLISECONDS).subscribe(intervalSubscriber);
    }

    private void publishRecentBeacons() {
        if (activeDevicesChanged() || activeDevices.isEmpty()) {
            scanner.onNext(activeDevices);
        }
    }

    private boolean activeDevicesChanged() {
        boolean activeDevicesChanged = false;
        for (ThunderBoardDevice device : devices) {
            long age = System.currentTimeMillis() - deviceAgeMap.get(device.getAddress());
            boolean deviceExpired = age > MAX_AGE;
            boolean deviceInList = activeDevices.contains(device);

            if (deviceExpired && deviceInList) {
                activeDevices.remove(device);
                activeDevicesChanged = true;
            } else if (!deviceExpired && !deviceInList) {
                activeDevices.add(device);
                activeDevicesChanged = true;
            }
        }
        return activeDevicesChanged;
    }


    void checkAvailableCharacteristics() {
        characteristicHallStateAvailable = false;
        characteristicHallFieldStrengthAvailable = false;
        characteristicCo2ReadingAvailable = false;
        characteristicTvocReadingAvailable = false;
        characteristicPressureAvailable = false;
        characteristicSoundLevelAvailable = false;
        characteristicHumidityAvailable = false;
        characteristicUvIndexAvailable = false;
        characteristicAmbientLightReactAvailable = false;
        characteristicAmbientLightSenseAvailable = false;

        if (gatt == null) {
            return;
        }

        for (BluetoothGattService service : gatt.getServices()) {
            if (service.getUuid().equals(UUID_SERVICE_ENVIRONMENT_SENSING)) {
                for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
                    if (characteristic.getUuid().equals(UUID_CHARACTERISTIC_HUMIDITY)) {
                        characteristicHumidityAvailable = true;
                    }
                    else if (characteristic.getUuid().equals(UUID_CHARACTERISTIC_UV_INDEX)) {
                        characteristicUvIndexAvailable = true;
                    }
                    else if (characteristic.getUuid().equals(UUID_CHARACTERISTIC_AMBIENT_LIGHT_REACT)) {
                        characteristicAmbientLightReactAvailable = true;
                    }
                    else if (characteristic.getUuid().equals(UUID_CHARACTERISTIC_PRESSURE)) {
                        characteristicPressureAvailable = true;
                    }
                    else if (characteristic.getUuid().equals(UUID_CHARACTERISTIC_SOUND_LEVEL)) {
                        characteristicSoundLevelAvailable = true;
                    }
                }
            }
            else if (service.getUuid().equals(UUID_SERVICE_AMBIENT_LIGHT)) {
                for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
                    if (characteristic.getUuid().equals(UUID_CHARACTERISTIC_AMBIENT_LIGHT_REACT)) {
                        characteristicAmbientLightReactAvailable = true;
                    }
                }
            }
            else if (service.getUuid().equals(UUID_SERVICE_HALL_EFFECT)) {
                for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
                    if (characteristic.getUuid().equals(UUID_CHARACTERISTIC_HALL_STATE)) {
                        characteristicHallStateAvailable = true;
                    }
                    else if (characteristic.getUuid().equals(UUID_CHARACTERISTIC_HALL_FIELD_STRENGTH)) {
                        characteristicHallFieldStrengthAvailable = true;
                    }
                }
            }
            else if (service.getUuid().equals(UUID_SERVICE_INDOOR_AIR_QUALITY)) {
                for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
                    if (characteristic.getUuid().equals(UUID_CHARACTERISTIC_CO2_READING)) {
                        characteristicCo2ReadingAvailable = true;
                    }
                    else if (characteristic.getUuid().equals(UUID_CHARACTERISTIC_TVOC_READING)) {
                        characteristicTvocReadingAvailable = true;
                    }
                }
            }
        }
    }
}