package com.eveningoutpost.dexdrip.Services;

import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.bluetooth.BluetoothDevice;
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.Build;
import android.os.Bundle;

import com.eveningoutpost.dexdrip.Models.JoH;
import com.eveningoutpost.dexdrip.Models.UserError;
import com.eveningoutpost.dexdrip.R;
import com.eveningoutpost.dexdrip.UtilityModels.Constants;
import com.eveningoutpost.dexdrip.UtilityModels.Inevitable;
import com.eveningoutpost.dexdrip.UtilityModels.Pref;
import com.eveningoutpost.dexdrip.UtilityModels.RxBleProvider;
import com.eveningoutpost.dexdrip.utils.BtCallBack;
import com.eveningoutpost.dexdrip.utils.BytesGenerator;
import com.eveningoutpost.dexdrip.utils.DisconnectReceiver;
import com.eveningoutpost.dexdrip.utils.bt.BtCallBack2;
import com.eveningoutpost.dexdrip.utils.bt.BtCallBack3;
import com.eveningoutpost.dexdrip.utils.bt.BtReconnect;
import com.eveningoutpost.dexdrip.utils.bt.ConnectReceiver;
import com.eveningoutpost.dexdrip.utils.bt.ReplyProcessor;
import com.eveningoutpost.dexdrip.utils.bt.Subscription;
import com.eveningoutpost.dexdrip.utils.framework.PoorMansConcurrentLinkedDeque;
import com.eveningoutpost.dexdrip.utils.time.SlidingWindowConstraint;
import com.eveningoutpost.dexdrip.watch.thinjam.BackgroundScanReceiver;
import com.eveningoutpost.dexdrip.xdrip;
import com.polidea.rxandroidble2.RxBleClient;
import com.polidea.rxandroidble2.RxBleConnection;
import com.polidea.rxandroidble2.RxBleDevice;
import com.polidea.rxandroidble2.RxBleDeviceServices;
import com.polidea.rxandroidble2.exceptions.BleAlreadyConnectedException;
import com.polidea.rxandroidble2.exceptions.BleDisconnectedException;
import com.polidea.rxandroidble2.scan.ScanFilter;
import com.polidea.rxandroidble2.scan.ScanSettings;
import com.rits.cloning.Cloner;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

import io.reactivex.schedulers.Schedulers;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

import static com.eveningoutpost.dexdrip.Models.JoH.emptyString;
import static com.eveningoutpost.dexdrip.Services.JamBaseBluetoothSequencer.BaseState.CLOSE;
import static com.eveningoutpost.dexdrip.Services.JamBaseBluetoothSequencer.BaseState.CLOSED;
import static com.eveningoutpost.dexdrip.Services.JamBaseBluetoothSequencer.BaseState.CONNECT_NOW;
import static com.eveningoutpost.dexdrip.Services.JamBaseBluetoothSequencer.BaseState.DISCOVER;
import static com.eveningoutpost.dexdrip.Services.JamBaseBluetoothSequencer.BaseState.INIT;
import static com.eveningoutpost.dexdrip.Services.JamBaseBluetoothSequencer.BaseState.SEND_QUEUE;
import static com.eveningoutpost.dexdrip.Services.JamBaseBluetoothSequencer.BaseState.SLEEP;
import static com.eveningoutpost.dexdrip.utils.bt.ScanMeister.SCAN_FOUND_CALLBACK;

// jamorham

public abstract class JamBaseBluetoothSequencer extends JamBaseBluetoothService implements BtCallBack, BtCallBack2, BtCallBack3 {

    private static final HashMap<UUID, String> mapToName = new HashMap<>();
    // protected final RxBleClient rxBleClient = RxBleProvider.getSingleton(this.getClass().getCanonicalName());
    protected final RxBleClient rxBleClient = RxBleProvider.getSingleton();
    private volatile String myid;
    protected volatile Inst I;

    protected BaseState mState;

    protected synchronized void setMyid(final String id) {
        UserError.Log.d(TAG, "Setting myid to: " + id);
        myid = id;
        I = Inst.get(id);
    }

    {
        setMyid(TAG);
    }


    // Instance management
    public static class Inst {

        private static final ConcurrentHashMap<String, Inst> singletons = new ConcurrentHashMap<>();


        private final PoorMansConcurrentLinkedDeque<QueueItem> write_queue = new PoorMansConcurrentLinkedDeque<>();

        public final ConcurrentHashMap<UUID, Object> characteristics = new ConcurrentHashMap<>();

        public volatile Subscription scanSubscription;
        public volatile Subscription connectionSubscription;
        public volatile Subscription stateSubscription;
        public volatile Subscription discoverSubscription;
        public volatile RxBleDevice bleDevice;
        public volatile RxBleConnection connection;

        public volatile String address; // use setAddress() to initiate! don't write directly
        public volatile boolean isConnected;
        public volatile boolean isNotificationEnabled;
        public volatile boolean isDiscoveryComplete;
        public volatile UUID readCharacteristic;
        public volatile UUID writeCharacteristic;
        public volatile String state;
        public volatile UUID queue_write_characterstic;
        public volatile long lastProcessedIncomingData = -1;
        public volatile int backgroundStepDelay = 100;
        public volatile int connectTimeoutMinutes = 7;

        public volatile boolean playSounds = false;
        public volatile boolean autoConnect = false;
        public volatile boolean autoReConnect = false;
        public volatile boolean useBackgroundScanning = false;
        public volatile boolean retry133 = true;
        public volatile boolean discoverOnce = false;
        public volatile boolean resetWhenAlreadyConnected = false;
        public volatile boolean useReconnectHandler = false;

        public PendingIntent serviceIntent;
        public SlidingWindowConstraint reconnectConstraint;
        private PendingIntent serviceFailoverIntent;
        public long retry_time;
        public long retry_backoff;
        public long wakeup_time;
        long failover_time;
        long last_wake_up_time;
        @Getter
        long lastConnected;


        private Inst() {
        }

        {
            state = INIT;
        }

        public static Inst get(final String id) {
            if (id == null) return null;
            final Inst conn = singletons.get(id);
            if (conn != null) return conn;
            synchronized (JamBaseBluetoothSequencer.class) {
                final Inst double_check = singletons.get(id);
                if (double_check != null) return double_check;
                final Inst singleton = new Inst();
                singletons.put(id, singleton);
                return singleton;
            }
        }

        public int getQueueSize() {
            return write_queue.size();
        }

    }

    // address handling

    protected void setAddress(String newAddress) {
        DisconnectReceiver.addCallBack(this, TAG);
        ConnectReceiver.addCallBack(this, TAG);
        if (emptyString(newAddress)) return;
        newAddress = newAddress.toUpperCase();

        if (!JoH.validateMacAddress(newAddress)) {
            final String msg = "Invalid MAC address: " + newAddress;
            if (JoH.quietratelimit("jam-invalid-mac", 60)) {
                UserError.Log.wtf(TAG, msg);
                JoH.static_toast_long(msg);
            }
            return;
        }

        if (I.address == null || !I.address.equals(newAddress)) {
            final String oldAddress = I.address;
            I.address = newAddress;
            newAddressEvent(oldAddress, newAddress);
        }
    }

    protected void newAddressEvent(final String oldAddress, final String newAddress) {

        // out with the old
        if (oldAddress != null) {
            stopConnect(oldAddress);
            stopWatching(oldAddress);
        }

        // in with the new
        I.bleDevice = rxBleClient.getBleDevice(newAddress);
        watchConnection(newAddress);
    }


    // connection handling
    private static final int SCAN_REQUEST_CODE = 142;   // just for unique pending intent


    public synchronized void btCallback2(final String mac, final String status, final String name, final Bundle bundle) {
        // currently we are only using this callback to implement faux auto-connect
        if (status.equals(SCAN_FOUND_CALLBACK)) {
            rxBleClient.getBackgroundScanner().stopBackgroundBleScan(scanCallBack);
            if (JoH.ratelimit("jambase-btcb2-" + mac, 2)) {
                stopConnect(mac);
                realEstablishConnection(mac, false); // don't auto connect as we did that via scan
            }
        }
    }


    private String getIntentFilterName() {
        return BackgroundScanReceiver.getACTION_NAME();
    }

    private PendingIntent scanCallBack = null;

    private void registerScanReceiver() {
        if (scanCallBack == null) {
            scanCallBack = PendingIntent.getBroadcast(xdrip.getAppContext(), SCAN_REQUEST_CODE,
                    new Intent(xdrip.getAppContext(), BackgroundScanReceiver.class).setAction(getIntentFilterName()).putExtra("CallingClass", this.getClass().getSimpleName()), PendingIntent.FLAG_UPDATE_CURRENT);
        }
        BackgroundScanReceiver.addCallBack2(this, this.getClass().getSimpleName());
    }

    private void unregisterScanReceiver() {
        if (scanCallBack != null) {
            try {
                rxBleClient.getBackgroundScanner().stopBackgroundBleScan(scanCallBack);
            } catch (Exception e) {
                UserError.Log.d(TAG, "Error removing background scanner callback: " + e);
            }
            BackgroundScanReceiver.removeCallBack(this.getClass().getSimpleName());
        }
    }

    protected synchronized void startConnect(final String address) {
        if (emptyString(address)) {
            UserError.Log.e(TAG, "Cannot connect as address is null");
            return;
        }
        if (address.equals("00:00:00:00:00:00")) {
            UserError.Log.d(TAG, "Not trying to connect to all zero mac");
            return;
        }
        if (I.isConnected) {
            UserError.Log.d(TAG, "Already connected  - skipping connect");
            changeNextState();
            return;
        }
        stopConnect(address); // or do something like check if we are already connected
        I.isConnected = false;

        resetBluetoothIfWeSeemToAlreadyBeConnected(address); // TODO might be a race condition here if we are already disconnecting - maybe we should check twice

        if (I.autoConnect && (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) && Pref.getBoolean("bluetooth_allow_background_scans", true)) {
            UserError.Log.d(TAG, "Trying background scan connect: " + scanCallBack + " " + address);
            try {

                rxBleClient.getBackgroundScanner()
                        .scanBleDeviceInBackground(scanCallBack,
                                new ScanSettings.Builder()
                                        .setScanMode(ScanSettings.SCAN_MODE_BALANCED)
                                        // .setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH) // doesn't work on samsung - annoying as could persist forever otherwise
                                        .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
                                        .build(),
                                new ScanFilter.Builder().setDeviceAddress(address).build());

            } catch (Exception e) {
                UserError.Log.e(TAG, "Cannot background scan: " + e);
            }

        } else {
            realEstablishConnection(address, I.autoConnect);
        }
    }

    // also called from callback
    private void realEstablishConnection(final String address, final boolean autoConnect) {
        UserError.Log.d(TAG, "Trying connect: " + address + " autoconnect: " + autoConnect);
        // Attempt to establish a connection
        I.connectionSubscription = new Subscription(I.bleDevice.establishConnection(autoConnect)
                .timeout(I.connectTimeoutMinutes, TimeUnit.MINUTES)
                // .flatMap(RxBleConnection::discoverServices)
                // .observeOn(AndroidSchedulers.mainThread())
                // .doOnUnsubscribe(this::clearSubscription)
                .subscribeOn(Schedulers.io())
                .doFinally(this::establishConnectionFinally)
                .subscribe(this::onConnectionReceived, this::onConnectionFailure));


    }

    private void establishConnectionFinally() {
        UserError.Log.d(TAG, "Establish connection finally called");
    }

    protected synchronized void stopConnect(final String address) {
        UserError.Log.d(TAG, "Stopping connection with: " + address);
        //UserError.Log.d(TAG, "Stopping connection with: " + address + backTrace());

        if (I.connectionSubscription != null) {
            I.connectionSubscription.unsubscribe();
            UserError.Log.d(TAG, "Unsubscribed in StopConnect");
        }
        stopDiscover();
        I.connection = null; // TODO IS THIS ACTUALLY CORRECT???
        I.isConnected = false;
    }

    protected synchronized void watchConnection(final String address) {
        UserError.Log.d(TAG, "Starting to watch connection with: " + address);
        /// / Listen for connection state changes
        I.stateSubscription = new Subscription(I.bleDevice.observeConnectionStateChanges()
                // .observeOn(AndroidSchedulers.mainThread())
                .subscribeOn(Schedulers.io())
                .subscribe(this::onConnectionStateChange, throwable -> {
                    UserError.Log.wtf(TAG, "Got Error from state subscription: " + throwable);
                }));
    }

    protected synchronized void stopWatching(final String address) {
        UserError.Log.d(TAG, "Stopping watching: " + address);
        if (I.stateSubscription != null) {
            I.stateSubscription.unsubscribe();
        }
    }


    // We have connected to the device!
    private void onConnectionReceived(RxBleConnection this_connection) {
        //msg("Connected");
        // TODO check connection already exists - close etc?
        I.connection = this_connection;
        I.lastConnected = JoH.tsl();
        I.isConnected = true;
        UserError.Log.d(TAG, "Initial connection going for service discovery");
        changeState(DISCOVER);
        if ((I.playSounds && (JoH.ratelimit("sequencer_connect_sound", 3)))) {
            JoH.playResourceAudio(R.raw.bt_meter_connect);
        }
    }

    private void onConnectionFailure(Throwable throwable) {
        UserError.Log.d(TAG, "received: onConnectionFailure: " + throwable);
        if (throwable instanceof BleAlreadyConnectedException) {
            UserError.Log.d(TAG, "Already connected - advancing to next stage");
            I.isConnected = true;
            changeNextState();
        } else if (throwable instanceof BleDisconnectedException) {
            if (((BleDisconnectedException) throwable).state == 133) {
                if (I.retry133) {
                    if (JoH.ratelimit(TAG + "133recon", 60)) {
                        if (I.state.equals(CONNECT_NOW)) {
                            UserError.Log.d(TAG, "Automatically retrying connection");
                            Inevitable.task(TAG + "133recon", 3000, new Runnable() {
                                @Override
                                public void run() {
                                    changeState(CONNECT_NOW);
                                }
                            });
                        }
                    }
                }
            } else if (I.autoConnect) {
                UserError.Log.d(TAG, "Auto reconnect persist");
                changeState(CONNECT_NOW);
            }
        }
    }

    @Override
    public void btCallback(String address, String status) {
        UserError.Log.d(TAG, "Processing callback: " + address + " :: " + status);
        if (I.address == null) return;
        if (address.equals(I.address)) {
            switch (status) {
                case "DISCONNECTED":
                    if (JoH.ratelimit("diconnected-from-" + address, 15)) {
                        //I.isConnected = false;
                        stopConnect(I.address);
                    } else {
                        UserError.Log.d(TAG, "Not processing disconnection callback due to debounce");
                    }
                    break;
                case "SCAN_FOUND":
                    break;
                case "SCAN_TIMEOUT":
                    break;
                case "SCAN_FAILED":
                    break;

                default:
                    UserError.Log.e(TAG, "Unknown status callback for: " + address + " with " + status);
            }
        } else {
            UserError.Log.d(TAG, "Ignoring: " + status + " for " + address + " as we are using: " + I.address);
        }
    }


    @Override
    public void btCallback3(final String mac, final String status, final String name, final Bundle bundle, final BluetoothDevice device) {
        UserError.Log.d(TAG, "Received callback: " + mac + " " + status);
        if (device != null && I.useReconnectHandler && device.getAddress().equals(I.address)) {
            BtReconnect.checkReconnect(device);
        }
    }

    protected synchronized void onConnectionStateChange(final RxBleConnection.RxBleConnectionState newState) {
        String connection_state = "Unknown";
        switch (newState) {
            case CONNECTING:
                connection_state = "Connecting";
                //  connecting_time = JoH.tsl();
                break;
            case CONNECTED:
                I.isConnected = true;
                I.retry_backoff = 0; // reset counter
                connection_state = "Connected";

                break;
            case DISCONNECTING:
                I.isConnected = false;
                connection_state = "Disconnecting";

                break;
            case DISCONNECTED:
                stopConnect(I.address);
                //I.isConnected = false;
                connection_state = "Disconnected";

                changeState(CLOSE);
                break;
        }

        UserError.Log.d(TAG, "Connection state changed to: " + connection_state);
    }


    // service discovery

    public synchronized void discover_services() {
        //  if (state == DISCOVER) {
        if (I.discoverOnce && I.isDiscoveryComplete) {
            UserError.Log.d(TAG, "Skipping service discovery as already completed");
            changeNextState();
        } else {
            if (I.connection != null) {
                UserError.Log.d(TAG, "Discovering services");
                stopDiscover();
                I.discoverSubscription = new Subscription(I.connection.discoverServices(10, TimeUnit.SECONDS)
                        .subscribe(this::onServicesDiscovered, this::onDiscoverFailed));
            } else {
                UserError.Log.e(TAG, "No connection when in DISCOVER state - reset");
                // These are normally just ghosts that get here, not really connected
                if (I.resetWhenAlreadyConnected) {
                    if (JoH.ratelimit("jam-sequencer-reset", 10)) {
                        changeState(CLOSE);
                    }
                }

            }
            //} else {
            //     UserError.Log.wtf(TAG, "Attempt to discover when not in DISCOVER state");
            //  }
        }
    }

    protected synchronized void stopDiscover() {
        if (I.discoverSubscription != null) {
            I.discoverSubscription.unsubscribe();
        }
    }


    protected void onServicesDiscovered(RxBleDeviceServices services) {
        UserError.Log.d(TAG, "Services discovered okay in base sequencer");
        final Object obj = new Object();
        for (BluetoothGattService service : services.getBluetoothGattServices()) {
            for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
                I.characteristics.put(characteristic.getUuid(), obj);
            }
        }
    }

    protected void onDiscoverFailed(Throwable throwable) {
        UserError.Log.e(TAG, "Discover failure: " + throwable.toString());
        tryGattRefresh(I.connection);
        changeState(CLOSE);
        // incrementErrors();
    }

// end service discovery


    // state automata's

    public static class BaseState {
        protected final List<String> sequence = new ArrayList<>();

        public static final String INIT = "Initializing";
        public static final String CONNECT_NOW = "Connecting";
        public static final String SEND_QUEUE = "Sending Queue";
        public static final String DISCOVER = "Discover Services";
        public static final String SLEEP = "Sleeping";
        public static final String CLOSE = "Closing";
        public static final String CLOSED = "Closed";

        private Inst LI;

        {
            sequence.add(INIT);
            sequence.add(CONNECT_NOW);
            sequence.add(SEND_QUEUE);
            //sequence.add(DISCOVER); // handled by initial connection callback
            sequence.add(SLEEP);
        }


        public BaseState setLI(final Inst LI) {
            this.LI = LI;
            return this;
        }


        public String next() {
            try {
                if (LI.state.equals(SLEEP))
                    return SLEEP; // will not auto-advance out of sleep state
                return sequence.get(sequence.indexOf(LI.state) + 1);
            } catch (Exception e) {
                return SLEEP;
            }
        }

    }


    public synchronized void changeState(final String new_state) {
        final String state = I.state;
        if (state == null) return;
        if ((state.equals(new_state)) && !state.equals(INIT) && !state.equals(SLEEP)) {
            if (!state.equals(CLOSE)) {
                UserError.Log.d(TAG, "Already in state: " + new_state.toUpperCase() + " changing to CLOSE");
                UserError.Log.d(TAG, JoH.backTrace());
                changeState(CLOSE);
            }
        } else {
            if ((state.equals(CLOSED) || state.equals(CLOSE)) && new_state.equals(CLOSE)) {
                UserError.Log.d(TAG, "Not closing as already closed");
            } else {
                UserError.Log.d(TAG, "Changing state from: " + state.toUpperCase() + " to " + new_state.toUpperCase());
                I.state = new_state;
                background_automata(I.backgroundStepDelay);
            }
        }
    }

    public void changeNextState() {
        changeState(mState.next());
    }


    protected synchronized boolean alwaysConnected() {
        if (I.isConnected || I.state.equals(CONNECT_NOW)) {
            UserError.Log.d(TAG, "Always connected passes");
            return true;
        }
        if (JoH.ratelimit(TAG + "auto-reconnect", 1)) {
            UserError.Log.d(TAG, "alwaysConnected() requesting connect");
            changeState(CONNECT_NOW);
        } else {
            UserError.Log.d(TAG, "Too frequent reconnect calls");
            setRetryTimerReal();
        }
        return false;
    }


    protected void setRetryTimerReal() {
        throw new RuntimeException("Must define setRetryTimerReal() if you are going to use it");
    }

    @Override
    protected synchronized boolean automata() {

        UserError.Log.d(TAG, "automata state: " + I.state);
        extendWakeLock(3000);
        try {
            switch (I.state) {
                case INIT:
                    UserError.Log.d(TAG, "INIT State does nothing unless overridden");
                    break;
                case CONNECT_NOW:
                    if (!I.isConnected) {
                        if (JoH.ratelimit("jambase connect" + I.address, 1)) {
                            startConnect(I.address);
                        } else {
                            UserError.Log.d(TAG, "Blocking duplicate connect within 1 second");
                        }
                    } else {
                        changeState(mState.next());
                    }
                    break;
                case DISCOVER:
                    discover_services();
                    break;
                case SEND_QUEUE:
                    startQueueSend();
                    break;

                case CLOSE:
                    stopConnect(I.address);
                    changeState(CLOSED);
                    break;

                case CLOSED:
                    if (I.autoReConnect) {
                        // TODO use sliding window constraint
                        if (I.reconnectConstraint != null) {
                            if (I.reconnectConstraint.checkAndAddIfAcceptable(1)) {
                                UserError.Log.d(TAG, "Attempting auto-reconnect");
                                changeState(CONNECT_NOW);
                            } else {
                                UserError.Log.d(TAG, "Not attempting auto-reconnect due to constraint");
                            }
                        } else {
                            UserError.Log.e(TAG, "No reconnectConstraint is null");
                        }

                    }
                    break;

                default:
                    return false;

            }
            return true;
        } finally {
            //
        }
    }

    // Queue


    /// Queue Handling

    @RequiredArgsConstructor
    private class QueueItem {
        final UUID queueWriteCharacterstic;
        final byte[] data;
        final int timeoutSeconds;
        final long post_delay;
        public final String description;
        final boolean expectReply;
        final long expireAt;
        int retries = 0;
        Runnable runnable;
        ReplyProcessor replyProcessor;
        BytesGenerator generator;

        boolean isExpired() {
            return expireAt != 0 && expireAt < JoH.tsl();
        }

        QueueItem setRunnable(Runnable runnable) {
            this.runnable = runnable;
            return this;
        }

        QueueItem setProcessor(ReplyProcessor processor) {
            this.replyProcessor = processor;
            return this;
        }

        QueueItem setGenerator(BytesGenerator generator) {
            this.generator = generator;
            return this;
        }

        byte[] getData() {
            if (data != null) {
                return data;
            } else {
                if (generator != null) {
                    return generator.produce();
                } else {
                    return null;
                }
            }
        }

    }

    private static final int MAX_QUEUE_RETRIES = 3;


    public class QueueMe {

        List<byte[]> byteslist;
        long delay_ms = 100;
        int timeout_seconds = 10;
        long expireAt;
        boolean start_now;
        boolean expect_reply;
        String description = "Vanilla Queue Item";
        UUID queueWriteCharacterstic;
        Runnable runnable;
        ReplyProcessor processor;
        BytesGenerator generator;


        public QueueMe setByteList(List<byte[]> byteList) {
            this.byteslist = byteList;
            return this;
        }

        public QueueMe setBytes(final byte[] bytes) {
            final List<byte[]> byteList = new LinkedList<>();
            byteList.add(bytes);
            this.byteslist = byteList;
            return this;
        }

        public QueueMe setTimeout(final int timeout) {
            this.timeout_seconds = timeout;
            return this;
        }

        public QueueMe setDelayMs(final int delay) {
            this.delay_ms = delay;
            return this;
        }

        public QueueMe expireInSeconds(final int timeout) {
            this.expireAt = JoH.tsl() + (Constants.SECOND_IN_MS * timeout);
            return this;
        }

        public QueueMe setDescription(String description) {
            this.description = description;
            return this;
        }

        public QueueMe setRunnable(Runnable runnable) {
            this.runnable = runnable;
            return this;
        }

        public QueueMe setProcessor(ReplyProcessor runnable) {
            this.processor = runnable;
            return this;
        }

        public QueueMe setGenerator(BytesGenerator generator) {
            this.generator = generator;
            return this;
        }

        public UUID getQueueWriteCharacterstic() {
            return queueWriteCharacterstic;
        }

        public QueueMe setQueueWriteCharacterstic(UUID queueWriteCharacterstic) {
            this.queueWriteCharacterstic = queueWriteCharacterstic;
            return this;
        }


        public QueueMe now() {
            this.start_now = true;
            return this;
        }

        public QueueMe expectReply() {
            this.expect_reply = true;
            return this;
        }

        public void queue() {
            this.start_now = false; // make sure disabled
            add();
        }

        public void queueUnique() {
            this.start_now = false; // make sure disabled
            add(true);
        }

        public void send() {
            this.start_now = true; // make sure enabled
            add();
        }

        private void add() {
            add(false); // don't unique by default
        }

        public void insert() {     // insert to head of queue
            addToWriteQueue(this, false, true);
        }

        private void add(final boolean unique) {
            //addToWriteQueue(byteslist, delay_ms, timeout_seconds, start_now, description, expect_reply, expireAt, runnable);
            addToWriteQueue(this, unique, false);
        }

    }

    private void addToWriteQueue(final QueueMe queueMe, final boolean unique, final boolean atHead) {
        Cloner cloner = null;
        if (queueMe.byteslist == null) {
            queueMe.byteslist = new LinkedList<>();
            if (queueMe.generator != null) {
                queueMe.byteslist.add(null); // create pseudo entry if this is using a generator
            }
        }
        final boolean multiple = queueMe.byteslist.size() > 1;
        for (final byte[] bytes : queueMe.byteslist) {
            if (unique) {
                if (doesWriteQueueContainBytes(bytes)) continue; // skip if duplicate bytes
            }
            ReplyProcessor replyProcessor = queueMe.processor;
            if (replyProcessor != null) {
                if (multiple) {
                    if (cloner == null) cloner = new Cloner();
                    replyProcessor = cloner.shallowClone(queueMe.processor);
                }
                if (replyProcessor != null) {
                    replyProcessor.setOutbound(bytes);
                } else {
                    UserError.Log.wtf(TAG, "Could not create clone of reply processor needed!!");
                }
            }
            UUID  queueWriteCharacterstic = queueMe.queueWriteCharacterstic;
            if (queueWriteCharacterstic == null) queueWriteCharacterstic = I.queue_write_characterstic;
            if (atHead) {
                I.write_queue.addFirst(new QueueItem(queueWriteCharacterstic, bytes, queueMe.timeout_seconds, queueMe.delay_ms, queueMe.description, queueMe.expect_reply, queueMe.expireAt)
                        .setRunnable(queueMe.runnable).setProcessor(replyProcessor).setGenerator(queueMe.generator));

            } else {
                I.write_queue.add(new QueueItem(queueWriteCharacterstic, bytes, queueMe.timeout_seconds, queueMe.delay_ms, queueMe.description, queueMe.expect_reply, queueMe.expireAt)
                        .setRunnable(queueMe.runnable).setProcessor(replyProcessor).setGenerator(queueMe.generator));

            }
        }
        if (queueMe.start_now) startQueueSend();
    }

    private boolean doesWriteQueueContainBytes(final byte[] bytes) {
        if (bytes == null) return false;
        synchronized (I.write_queue) {
            // TODO a more efficient way to do this
            for (final QueueItem item : I.write_queue.toArray(new QueueItem[1])) {
                if (item != null) {
                    if (!item.isExpired()) {
                        if (Arrays.equals(bytes, item.data)) return true;
                    }
                }
            }
        }
        return false;
    }

    public void emptyQueue() {
        I.write_queue.clear();
    }


    public void startQueueSend() {
        Inevitable.task("sequence-start-queue " + I.address, 0, new Runnable() {
            @Override
            public void run() {
                writeMultipleFromQueue(I.write_queue);
            }
        });
    }

    private synchronized void writeMultipleFromQueue(final PoorMansConcurrentLinkedDeque<QueueItem> queue) {
        if (I.isConnected) {
            QueueItem item = queue.poll();
            while (item != null && item.isExpired()) {
                UserError.Log.d(TAG, "Item expired from queue early: (expiry: " + JoH.dateTimeText(item.expireAt) + " " + item.description);
                item = queue.poll();
            }
            if (item != null) {
                if (!item.isExpired()) {
                    UserError.Log.d(TAG, "Starting queue send for item: " + item.description);
                    writeQueueItem(queue, item);
                } else {
                    // TODO this is very much an edge case now
                    UserError.Log.d(TAG, "Item expired from queue: (expiry: " + JoH.dateTimeText(item.expireAt) + " " + item.description);
                    writeMultipleFromQueue(queue);
                }
            } else {
                UserError.Log.d(TAG, "write queue empty");
                changeState(mState.next()); // check if this logic is sound
            }
        } else {
            UserError.Log.d(TAG, "CANNOT WRITE QUEUE AS DISCONNECTED");
        }
    }


    @SuppressLint("CheckResult")
    private void writeQueueItem(final PoorMansConcurrentLinkedDeque<QueueItem> queue, final QueueItem item) {
        extendWakeLock(2000 + item.post_delay);
        if (I.connection == null) {
            UserError.Log.e(TAG, "Cannot write queue item: " + item.description + " as we have no connection!");
            return;
        }

        if (item.queueWriteCharacterstic == null) {
            UserError.Log.e(TAG, "Write characteristic not set in queue write");
            return;
        }
        UserError.Log.d(TAG, "Writing to characteristic: " + item.queueWriteCharacterstic + " " + item.description);
        I.connection.writeCharacteristic(item.queueWriteCharacterstic, item.getData())
                .timeout(item.timeoutSeconds, TimeUnit.SECONDS)
                .subscribe(Value -> {
                    UserError.Log.d(TAG, "Wrote request: " + item.description + " -> " + JoH.bytesToHex(Value));
                    if (item.expectReply) expectReply(queue, item);
                    if (item.post_delay > 0) {
                        // always sleep if set as new item might appear in queue
                        final long sleep_time = item.post_delay + (item.description.contains("WAKE UP") ? 2000 : 0);
                        if (sleep_time != 100) UserError.Log.d(TAG, "sleeping " + sleep_time);
                        JoH.threadSleep(sleep_time);
                    }
                    if (item.runnable != null) {
                        item.runnable.run(); // TODO should this be handled by expect reply in that case?
                    }
                    if (!item.expectReply) {
                        if (item.replyProcessor != null) {
                            item.replyProcessor.process(Value);
                        }
                        writeMultipleFromQueue(queue); // start next item immediately
                    }
                    throw new OperationSuccess("write complete: " + item.description);
                }, throwable -> {
                    if (!(throwable instanceof OperationSuccess)) {
                        UserError.Log.d(TAG, "Throwable in: " + item.description + " -> " + throwable);
                        item.retries++;
                        if (!(throwable instanceof BleDisconnectedException)) {
                            if (item.retries > MAX_QUEUE_RETRIES) {
                                UserError.Log.d(TAG, item.description + " failed max retries @ " + item.retries + " shutting down queue");
                                queue.clear(); /// clear too?
                                //changeState(CLOSE); // TODO put on switch
                            } else {
                                writeQueueItem(queue, item);
                            }
                        } else {
                            UserError.Log.d(TAG, "Disconnected so not attempting retries");
                            I.isConnected = false;
                        }
                    } else {
                        // not disconnecting on success
                    }
                });
    }

    private void expectReply(final PoorMansConcurrentLinkedDeque<QueueItem> queue, final QueueItem item) {
        final long wait_time = 3000;
        Inevitable.task("expect-reply-" + I.address + "-" + item.description, wait_time, new Runnable() {
            @Override
            public void run() {
                if (JoH.msSince(I.lastProcessedIncomingData) > wait_time) {
                    UserError.Log.d(TAG, "GOT NO REPLY FOR: " + item.description + " @ " + item.retries);
                    item.retries++;
                    if (item.retries <= MAX_QUEUE_RETRIES) {
                        UserError.Log.d(TAG, "Retrying due to no reply: " + item.description);
                        writeQueueItem(queue, item);
                    }
                }
            }
        });
    }

    // Life Cycle

    // Turn off anything we may have turned on
    protected void shutDown() {
        stopConnect(I.address);
        stopWatching(I.address);
    }

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


    @Override
    public void onDestroy() {
        shutDown();
        DisconnectReceiver.removeCallBack(TAG);
        ConnectReceiver.removeCallBack(TAG);
        unregisterScanReceiver();
        super.onDestroy();
    }


    // utils

    public static String getUUIDName(final UUID uuid) {
        if (uuid == null) return "null";
        final String result = mapToName.get(uuid);
        return result != null ? result : "Unknown uuid: " + uuid.toString();
    }

    // does the system think we are connected to a device
    public static boolean isConnectedToDevice(final String mac) {
        if (JoH.emptyString(mac)) {
            return false;
        }
        final BluetoothManager bluetoothManager = (BluetoothManager) xdrip.getAppContext().getSystemService(Context.BLUETOOTH_SERVICE);
        if (bluetoothManager == null) {
            return false;
        }
        boolean foundConnectedDevice = false;
        for (BluetoothDevice bluetoothDevice : bluetoothManager.getConnectedDevices(BluetoothProfile.GATT)) {
            if (bluetoothDevice.getAddress().equalsIgnoreCase(mac)) {
                foundConnectedDevice = true;
                break;
            }
        }
        return foundConnectedDevice;
    }

    public void resetBluetoothIfWeSeemToAlreadyBeConnected(final String mac) {
        if (isConnectedToDevice(mac)) {
            if (Pref.getBooleanDefaultFalse("bluetooth_watchdog")) {
                if (JoH.ratelimit("jamsequencer-restart-bluetooth", 1200)) {
                    UserError.Log.e(TAG, "Restarting bluetooth as device reports we are connected but we can't find our connection");
                    JoH.niceRestartBluetooth(xdrip.getAppContext());
                } else {
                    UserError.Log.d(TAG, "Cannot restart bluetooth due to rate limit but we seem to be connected");
                }
            }
        }
    }

}