package com.theksmith.android.car_bus_interface;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.app.TaskStackBuilder;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.util.Log;

import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.theksmith.android.car_bus_interface.BusData.*;


/*
todo: improve handling of potential error conditions...

 -  if connected ok, but not receiving for a certain time period, maybe should try a reset (ATWS or ATZ) at some point?
    we would need a setting for if/when as some cars might not send anything for long periods
    otherwise is there a better way to check the real current health of the device/socket?

 -  after certain number of connection attempts should we give up? (i.e. don't keep trying to re-connect forever)

*/

/**
 * primary foreground Service which runs even after main activity is destroyed
 * attempts to connect with the bluetooth interface device, send commands, then listens and processes any responses
 *
 * @author Kristoffer Smith <[email protected]>
 */
public class CBIServiceMain extends Service {
    private static final String TAG = "CBIServiceMain";
    private static final boolean D = BuildConfig.SHOW_DEBUG_LOG_LEVEL > 0;
    private static final boolean DD = BuildConfig.SHOW_DEBUG_LOG_LEVEL > 1;

    private final Messenger mBoundIncomingMessenger = new Messenger(new BoundIncomingHandler());
    private ArrayList<Messenger> mBoundClients = new ArrayList<Messenger>();

    public static final int BOUND_MSG_REGISTER_CLIENT = 1;
    public static final int BOUND_MSG_UNREGISTER_CLIENT = 2;
    public static final int BOUND_MSG_NOTIFY_BUS_DATA = 3;
    public static final int BOUND_MSG_SEND_BUS_COMMAND = 4;
    public static final int BOUND_MSG_SEND_STARTUP_COMMANDS = 5;

    private SharedPreferences mSettings;

    private static final int PERSISTENT_NOTIFICATION_ID = 0;

    private NotificationManager mNoticeManager;
    private Notification.Builder mNoticeBuilder;

    private String mNoticeStatus;
    private String mNoticeError;

    private static enum BTState {
        DESTROYING, NONE, CONNECTING, IDLE, RX, TX
    }

    private volatile BTState mBTState = BTState.NONE;

    private volatile BluetoothAdapter mBTAdapter;
    private BTConnectThread mBTConnectThread;
    private BTIOThread mBTIOThread;

    private static final long BT_CONNECTION_RETRY_WAIT = 2000; //milliseconds

    //this matches the termination of a MESSAGE (could be multiple within a response) or the entire RESPONSE
    //explained: match "\r" or ">" at a minimum and also variations of " \r\n >" while also trying to trim out extra spaces, CRs, and LFs
    private static final String ELM_RESPONSE_SEPARATOR_REGEX = " *\\r+\\n* *\\r*\\n* *| *\\r*\\n* *> *\\r*\\n* *|>";

    //consider an entire RESPONSE complete when this string occurs
    private static final String ELM_RESPONSE_TERMINATOR = ">";

    private final static String ELM_COMMAND_TERMINATOR = "\r\n";
    private final static long ELM_COMMAND_QUEUE_BUSY_WAIT_TIME = 100; //milliseconds

    private String mELMResponseBuffer;
    private ELMCommandQueueThread mELMCommandQueueThread;

    private HashMap<String, BusMessageProcessor> mBusMsgProcessors;


    public CBIServiceMain() {
        if (D) Log.d(TAG, "CBIServiceMain()");

        mBTAdapter = BluetoothAdapter.getDefaultAdapter();
    }

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

        if (D) Log.d(TAG, "onCreate()");

        //create this as we need to get user preferences several times
        mSettings = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());

        //setup a receiver to watch for bluetooth adapter state changes
        final IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
        this.registerReceiver(mBTStateReceiver, filter);

        //setup the persistent notification as required for any service that returns START_STICKY

        final Intent intent = new Intent(this, CBIActivityMain.class);
        intent.setAction(Intent.ACTION_EDIT);
        final TaskStackBuilder stack = TaskStackBuilder.create(this);
        stack.addParentStack(CBIActivityMain.class);
        stack.addNextIntent(intent);
        PendingIntent resultPendingIntent = stack.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);

        mNoticeBuilder = new Notification.Builder(this);

        mNoticeBuilder.setOngoing(true);
        mNoticeBuilder.setPriority(Notification.PRIORITY_LOW);
        mNoticeBuilder.setContentIntent(resultPendingIntent);
        mNoticeBuilder.setSmallIcon(R.drawable.ic_notice);
        mNoticeBuilder.setContentTitle(getString(R.string.app_name));

        mNoticeStatus = getString(R.string.msg_starting);
        mNoticeBuilder.setContentText(mNoticeStatus);

        mNoticeManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
        mNoticeManager.notify(PERSISTENT_NOTIFICATION_ID, mNoticeBuilder.build());
    }

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

        if (D) Log.d(TAG, "onDestroy()");

        mBTState = BTState.DESTROYING;

        this.unregisterReceiver(mBTStateReceiver);

        stop();

        mNoticeManager.cancelAll();
    }

    @Override
    public int onStartCommand(final Intent intent, final int flags, final int startId) {
        super.onStartCommand(intent, flags, startId);

        if (D) Log.d(TAG, "onStartCommand()");

        startForeground(PERSISTENT_NOTIFICATION_ID, mNoticeBuilder.build());

        //multiple calls to startService() for this service shouldn't actually restart everything if it's already running smoothly
        if (!isBTConnected()) {
            start();
        }

        //this tells the system to keep the service running
        //it could still be killed due to low resources, but would automatically be re-started when possible
        //in that situation onStartCommand() is called with a null intent (if there are not already other pending start requests)
        return START_STICKY;
    }

    @Override
    public IBinder onBind(Intent intent) {
        if (D) Log.d(TAG, "onBind()");

        return mBoundIncomingMessenger.getBinder();
    }

    private boolean isBound() {
        return mBoundClients != null && mBoundClients.size() > 0;
    }

    private void BoundNotifyBusData(final BusData data) {
        if (DD) Log.d(TAG, "BoundNotifyBusData() : data.data= " + data.data);

        if (isBound()) {
            for (int i = mBoundClients.size() - 1; i >= 0; i--) {
                try {
                    mBoundClients.get(i).send(Message.obtain(null, BOUND_MSG_NOTIFY_BUS_DATA, data));
                } catch (RemoteException e) {
                    //this client is no longer connected, remove it from the list
                    mBoundClients.remove(i);
                }
            }
        }
    }

    private void BoundNotifyNotReady() {
        BusData data = new BusData(getString(R.string.msg_error_bound_error_prefix) + " | " + getNotificationText(), BusDataType.ERROR, false);
        BoundNotifyBusData(data);
    }

    class BoundIncomingHandler extends Handler {
        @Override
        public void handleMessage(Message message) {
            if (D) Log.d(TAG, "BoundIncomingHandler : handleMessage() : msg.what= " + message.what);

            synchronized (CBIServiceMain.this) {
                switch (message.what) {
                    case BOUND_MSG_REGISTER_CLIENT:
                        mBoundClients.add(message.replyTo);
                        break;

                    case BOUND_MSG_UNREGISTER_CLIENT:
                        mBoundClients.remove(message.replyTo);
                        break;

                    case BOUND_MSG_SEND_BUS_COMMAND:
                        if (!isBTConnected()) {
                            BoundNotifyNotReady();
                        } else {
                            elmSendCommand(message.obj.toString());
                        }

                        break;

                    case BOUND_MSG_SEND_STARTUP_COMMANDS:
                        if (!isBTConnected()) {
                            BoundNotifyNotReady();
                        } else {
                            elmInitStartupCommands();
                        }

                        break;

                    default:
                        super.handleMessage(message);
                }
            }
        }
    }

    private synchronized void start() {
        if (D) Log.d(TAG, "start()");

        cancelAllThreads();

        if (mBTState == BTState.DESTROYING) {
            stopSelf();
            return;
        }

        if (mBTAdapter == null || !mBTAdapter.isEnabled()) {
            btNotEnabled();
            return;
        }

        final String address = mSettings.getString("bluetooth_mac", "");
        if (address.equals("")) {
            btNotConfigured();
            return;
        }

        mBTState = BTState.NONE;

        BluetoothDevice device = mBTAdapter.getRemoteDevice(address);
        btConnect(device);
    }

    private synchronized void stop() {
        if (D) Log.d(TAG, "stop()");

        cancelAllThreads();

        if (mBTState == BTState.DESTROYING) {
            stopSelf();
            return;
        }

        mBTState = BTState.NONE;
    }

    private void cancelAllThreads() {
        if (D) Log.d(TAG, "cancelAllThreads()");

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

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

        elmDestroyCommandQueue();

        if (mBusMsgProcessors != null) {
            for (String msg : mBusMsgProcessors.keySet()) {
                BusMessageProcessor processor = mBusMsgProcessors.get(msg);
                if (processor != null) {
                    processor.cancel();
                }
            }
            mBusMsgProcessors = null;
        }
    }

    /**
     * update the text for the persistent notification associated with this service
     *
     * @param status  status text, empty to clear or null to keep existing value
     * @param error  error text, empty to clear, or null to keep existing value
     */
    private synchronized void setNotificationText(final String status, final String error) {
        if (status != null) {
            mNoticeStatus = status;
        }

        if (error != null) {
            mNoticeError = error;
        }

        mNoticeBuilder.setContentText(getNotificationText());

        mNoticeManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
        mNoticeManager.notify(PERSISTENT_NOTIFICATION_ID, mNoticeBuilder.build());
    }

    private String getNotificationText() {
        String text = mNoticeStatus == null ? "" : mNoticeStatus;
        if (mNoticeStatus != null && !mNoticeStatus.equals("") && mNoticeError != null && !mNoticeError.equals("")) {
            text += " | ";
        }
        text += mNoticeError == null ? "" : mNoticeError;

        return text;
    }

    private synchronized boolean isBTConnected() {
        return mBTState == BTState.IDLE || mBTState == BTState.RX || mBTState == BTState.TX;
    }

    private synchronized void btConnect(final BluetoothDevice device) {
        if (D) Log.d(TAG, "btConnect()");

        if (mBTState == BTState.DESTROYING) {
            stopSelf();
            return;
        }

        mBTConnectThread = new BTConnectThread(device);
        mBTConnectThread.start();

        mBTState = BTState.CONNECTING;

        setNotificationText(getString(R.string.msg_connecting) + " " + device.getName() + "...", "");
    }

    private synchronized void btConnected(final BluetoothSocket socket, final BluetoothDevice device) {
        if (D) Log.d(TAG, "btConnected()");

        if (mBTState == BTState.DESTROYING) {
            stopSelf();
            return;
        }

        mBTConnectThread = null;

        mBTIOThread = new BTIOThread(socket);
        mBTIOThread.start();

        mBTState = BTState.IDLE;

        setNotificationText(getString(R.string.msg_connected) + " " + device.getName(), "");

        elmInit();
    }

    private synchronized void btReceivedData(final byte[] buffer, final int length) {
        if (DD) Log.d(TAG, "btReceivedData()");

        //flag state as RX
        //this will only change back to IDLE once the RX is found to be _complete_
        mBTState = BTState.RX;

        elmBufferData(new String(buffer, 0, length));
    }

    private synchronized void btWriteData(final byte[] data) {
        if (D) Log.d(TAG, "btWriteData()");

        //flag state as TX
        //since we always expect some response from a command, this state will only change once an RX is received
        mBTState = BTState.TX;

        mBTIOThread.write(data);
    }

    private synchronized void btWriteBreak() {
        if (D) Log.d(TAG, "btWriteBreak()");

        //we don't use btWriteData() here as we don't want to set mBTState to TX since this special case may never have a corresponding complete RX event to return mBTState to IDLE
        //no longer sending just a null character here, see issue #10 on github for details
        final String data = "\t" + ELM_COMMAND_TERMINATOR;
        mBTIOThread.write(data.getBytes());

        //give the device time to realize the break before whatever called this method tries to continue
        SystemClock.sleep(250);
    }

    private void btNotEnabled() {
        if (D) Log.d(TAG, "btNotEnabled()");

        setNotificationText(getString(R.string.msg_stopped), getString(R.string.msg_bt_not_enabled));
        stop();
    }

    private void btNotPaired() {
        if (D) Log.d(TAG, "btNotEnabled()");

        setNotificationText(getString(R.string.msg_stopped), getString(R.string.msg_bt_not_paired));
        stop();
    }

    private void btNotConfigured() {
        if (D) Log.d(TAG, "btNotConfigured()");

        setNotificationText(getString(R.string.msg_stopped), getString(R.string.msg_bt_not_configured));
        stop();
    }

    private void btConnectionFailed() {
        if (D) Log.d(TAG, "btConnectionFailed()");

        start();
    }

    private void btConnectionLost() {
        if (D) Log.d(TAG, "btConnectionLost()");

        start();
    }

    private final BroadcastReceiver mBTStateReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(final Context context, final Intent intent) {
            if (intent.getAction().equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
                if (D) Log.d(TAG, "mBTStateReceiver : onReceive()");

                final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);

                if (state == BluetoothAdapter.STATE_OFF || state == BluetoothAdapter.STATE_TURNING_OFF) {
                    btNotEnabled();
                } else if (state == BluetoothAdapter.STATE_ON) {
                    CBIServiceMain.this.start();
                }
            }
        }
    };

    private class BTConnectThread extends Thread {
        private volatile boolean mmCancelling;
        private volatile BluetoothSocket mmSocket;
        private volatile BluetoothDevice mmDevice;

        public BTConnectThread(final BluetoothDevice device) {
            if (D) Log.d(TAG, "BTConnectThread.BTConnectThread()");

            mmDevice = device;
            BluetoothSocket tmpSocket = null;

            try {
                if (mmDevice == null || mmDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
                    btNotPaired();
                    return;
                }

                /*
                //for some OBD2 dongles you need to specify a "channel"
                //the Android public API for a BluetoothDevice doesn't currently expose a socket creation method with that capability

                //so using createRfcommSocketToServiceRecord() with the standard SPP UUID does not work:
                UUID uuidSPP = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
                tmpSocket = mmDevice.createRfcommSocketToServiceRecord(uuidSPP);

                //nor even trying each of the devices advertised UUIDs
                for (ParcelUuid uuid : device.getUuids()) {
                    tmpSocket = mmDevice.createRfcommSocketToServiceRecord(uuid.getUuid());
                    sleep(3000);
                    if (tmpSocket.isConnected()) break;
                }

                //instead we use reflection to access the hidden public method createRfcommSocket()
                //channel 1 seems pretty universal with these type of devices

                //todo: should we try the documented methods for a BT socket first and then only resort to reflection as a backup?
                */
                final Method m = mmDevice.getClass().getMethod("createRfcommSocket", new Class[] {int.class});
                tmpSocket = (BluetoothSocket) m.invoke(mmDevice, 1);
            } catch (Exception e) {
                Log.w(TAG, "BTConnectThread.BTConnectThread() : failed to establish socket : exception= " + e.getMessage(), e);
            }

            mmSocket = tmpSocket;
        }

        @Override
        public void run() {
            if (D) Log.d(TAG, "BTConnectThread.run()");

            if (mmCancelling) {
                return;
            }

            try {
                //doing this BEFORE the connect attempt should ensure that regardless of how we got here we don't try to re-connect too fast which can give the exception "RFCOMM_CreateConnection - already opened state:2, RFC state:4, MCB state:5"
                //we were only doing this in the catch below before the call to btConnectionFailed()
                SystemClock.sleep(CBIServiceMain.BT_CONNECTION_RETRY_WAIT);

                mmSocket.connect();

                CBIServiceMain.this.btConnected(mmSocket, mmDevice);
            } catch (Exception e) {
                Log.w(TAG, "BTConnectThread.run() : failed to connect : exception= " + e.getMessage(), e);

                if (mmSocket != null && mmSocket.isConnected()) {
                    try {
                        mmSocket.close();
                    } catch (Exception e2) {
                        Log.w(TAG, "BTConnectThread.run() : failed to close socket after connection failure : exception= " + e2.getMessage(), e2);
                    }
                }

                CBIServiceMain.this.btConnectionFailed();
            }
        }

        public void cancel() {
            if (D) Log.d(TAG, "BTConnectThread.cancel()");

            mmCancelling = true;

            if (mmSocket != null && mmSocket.isConnected()) {
                try {
                    mmSocket.close();
                } catch (Exception e) {
                    Log.w(TAG, "BTConnectThread.cancel() : failed to close socket : exception= " + e.getMessage(), e);
                }
            }
        }
    }

    private class BTIOThread extends Thread {
        private volatile boolean mmCancelling;
        private volatile BluetoothSocket mmSocket;
        private volatile InputStream mmInStream;
        private volatile OutputStream mmOutStream;

        public BTIOThread(final BluetoothSocket socket) {
            if (D) Log.d(TAG, "BTIOThread.BTIOThread()");

            mmSocket = socket;
            InputStream tmpIn = null;
            OutputStream tmpOut = null;

            try {
                tmpIn = mmSocket.getInputStream();
                tmpOut = mmSocket.getOutputStream();
            } catch (Exception e) {
                Log.w(TAG, "BTIOThread.BTIOThread() : failed to obtain io streams : exception= " + e.getMessage(), e);
            }

            mmInStream = tmpIn;
            mmOutStream = tmpOut;
        }


        @Override
        public void run() {
            if (D) Log.d(TAG, "BTIOThread.run()");

            byte[] buffer = new byte[1024];
            int length;

            while (!mmCancelling && mmInStream != null) {
                try {
                    //note: only performing the read if mmInStream.available() > 0 did NOT work reliably - long running RX operations would start returning 0 constantly after about a minute
                    length = mmInStream.read(buffer);
                    CBIServiceMain.this.btReceivedData(buffer.clone(), length);
                } catch (Exception e) {
                    Log.w(TAG, "BTIOThread.run() : exception while reading : exception= " + e.getMessage(), e);

                    CBIServiceMain.this.btConnectionLost();
                    return;
                }
            }

            if (!mmCancelling) {
                Log.w(TAG, "BTIOThread.run() : lost input stream");

                CBIServiceMain.this.btConnectionLost();
            }
        }

        public void write(final byte[] buffer) {
            if (D) Log.d(TAG, "BTIOThread.write()");

            if (mmCancelling) {
                return;
            }

            try {
                mmOutStream.write(buffer);
                mmOutStream.flush();
            } catch (Exception e) {
                Log.w(TAG, "BTIOThread.write() : exception while writing : exception= " + e.getMessage(), e);
            }
        }

        public void cancel() {
            if (D) Log.d(TAG, "BTIOThread.cancel()");

            mmCancelling = true;

            if (mmSocket != null && mmSocket.isConnected()) {
                try {
                    mmSocket.close();
                } catch (Exception e) {
                    Log.w(TAG, "BTIOThread.cancel() : failed to close socket : exception= " + e.getMessage(), e);
                }
            }
        }
    }

    private void elmBadConfig(final String noticeErrorText) {
        if (D) Log.d(TAG, "elmBadConfig()");

        setNotificationText(getString(R.string.msg_stopped), noticeErrorText);
        stop();
    }

    private synchronized void elmInit() {
        if (D) Log.d(TAG, "elmInit()");

        /*
        FYI: you don't have to make this call to setup the processors if you don't need handle repeating messages (to skip bounces, identify short/long/double-press type scenarios, etc.)
        instead you could just setup a case statement in the elmParseResponse() method to respond to messages as they come in
        */
        elmInitBusMsgProcessors();

        elmInitStartupCommands();
    }

    private synchronized void elmInitBusMsgProcessors() {
        if (D) Log.d(TAG, "elmInitBusMsgProcessors()");

        //todo: the way we are storing these preferences is a quick hack, we need a custom preference screen to configure any number of these

        mBusMsgProcessors = new HashMap<String, BusMessageProcessor>();

        final Context appContext = getApplicationContext();

        BusMessageProcessor processor;
        String monitorSetting;
        String[] monitorArgs;

        String msg;
        boolean silenceErrors;
        long bounceTime;
        long shortTime;
        long longTime;
        long longWatchTime;
        String shortAction;
        String longAction;

        for (int m = 1; m <= 10; m++) {
            try {
                monitorSetting = mSettings.getString("elm_monitor" + m, "");
                if (!monitorSetting.equals("")) {
                    monitorArgs = monitorSetting.split("\\|");

                    msg = monitorArgs[0].trim();
                    silenceErrors = Boolean.parseBoolean(monitorArgs[1].trim());
                    bounceTime = Long.parseLong(monitorArgs[2].trim(), 10);
                    shortTime = Long.parseLong(monitorArgs[3].trim(), 10);
                    longTime = Long.parseLong(monitorArgs[4].trim(), 10);
                    longWatchTime = Long.parseLong(monitorArgs[5].trim(), 10);
                    shortAction = monitorArgs[6].trim();
                    longAction = monitorArgs[7].trim();

                    processor = new BusMessageProcessor(appContext, msg, silenceErrors, bounceTime, shortTime, longTime, longWatchTime, shortAction, longAction);
                    processor.start();
                    mBusMsgProcessors.put(msg, processor);
                }
            } catch (Exception e) {
                Log.w(TAG, "elmInit() : exception while setting up data processors : monitor #" + m + " : exception= " + e.getMessage(), e);

                elmBadConfig(getString(R.string.msg_bus_monitors_not_configured));
                return;
            }
        }

        if (mBusMsgProcessors == null || mBusMsgProcessors.size() <= 0) {
            Log.w(TAG, "elmInit() : no data processors");

            elmBadConfig(getString(R.string.msg_bus_monitors_not_configured));
        }
    }

    private synchronized void elmInitStartupCommands() {
        if (D) Log.d(TAG, "elmInitStartupCommands()");

        final String prefElmCommands = mSettings.getString("elm_commands", "");
        if (prefElmCommands.equals("")) {
            Log.w(TAG, "elmInit() : no startup commands");

            elmBadConfig(getString(R.string.msg_bus_commands_not_configured));
            return;
        }

        final String[] commands = prefElmCommands.split("; *");
        if (commands.length <= 0) {
            Log.w(TAG, "elmInit() : invalid startup commands");

            elmBadConfig(getString(R.string.msg_bus_commands_not_configured));
            return;
        }

        elmDestroyCommandQueue();
        btWriteBreak();

        for (String command : commands) {
            elmQueueCommand(command.trim());
        }
    }

    private synchronized void elmQueueCommand(String command) {
        if (D) Log.d(TAG, "elmQueueCommand() : command= " + command);

        if (command == null || command.equals("")) {
            return;
        }

        if (mELMCommandQueueThread == null) {
            mELMCommandQueueThread = new ELMCommandQueueThread();
            mELMCommandQueueThread.start();
        }

        mELMCommandQueueThread.add(command);
    }

    private synchronized void elmSendCommand(String command) {
        if (D) Log.d(TAG, "elmSendCommand() : command= " + command);

        if (command == null || command.equals("")) {
            return;
        }

        if (!isBTConnected()) {
            Log.w(TAG, "elmSendCommand() : failed to send command (bluetooth not connected)");

            btConnectionLost();
            return;
        }

        //alert any bound clients of this TX
        BusData data = new BusData(command, BusDataType.TX, false);
        BoundNotifyBusData(data);

        if (mBTState != BTState.IDLE) {
            //we are in the middle of something (like a long RX)
            btWriteBreak();
        }

        command += ELM_COMMAND_TERMINATOR;
        btWriteData(command.getBytes());
    }

    private void elmDestroyCommandQueue() {
        if (D) Log.d(TAG, "elmDestroyCommandQueue()");

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

    private synchronized void elmBufferData(final String data) {
        if (DD) Log.d(TAG, "elmBufferData() : data= " + data);

        if (mELMResponseBuffer == null) {
            mELMResponseBuffer = "";
        }

        final Pattern p = Pattern.compile(ELM_RESPONSE_SEPARATOR_REGEX);
        final Matcher m = p.matcher(data);

        if (m.find()) {
            //the data contained a separator or terminator
            final String part1 = data.substring(0, m.start());
            mELMResponseBuffer += part1;
            boolean completed = m.group().contains(ELM_RESPONSE_TERMINATOR);
            elmParseResponse(mELMResponseBuffer, completed);
            mELMResponseBuffer = "";

            final String part2 = data.substring(m.end());

            if (completed && part2.equals("")) {
                //the data was a clean end to a response (terminator with no trailing data)
                mBTState = BTState.IDLE;
            } else if (!part2.equals("")) {
                //the data continued after the separator or terminator
                elmBufferData(part2);
            }
        } else {
            //the response is not yet complete so keep appending data to the buffer
            mELMResponseBuffer += data;
        }
    }

    private synchronized void elmParseResponse(String response, final boolean completed) {
        response = response.replaceAll("[\\r\\n]", "");
        response = response.trim();

        if (D) Log.d(TAG, "elmParseResponse() : response= " + response + " completed= " + completed);

        BusDataType messageType = BusDataType.RX;

        /*
        FYI: here is where you would handle specific bus messages directly if you didn't need the BusMessageProcessor system
        */

        BusMessageProcessor processor = mBusMsgProcessors.get(response);
        if (processor != null) {
            messageType = BusDataType.RX_MONITORED;
            processor.logEvent();
        }

        //alert any bound clients of this RX
        BusData data = new BusData(response, messageType, completed);
        BoundNotifyBusData(data);
    }

    private class ELMCommandQueueThread extends Thread {
        private volatile boolean mmCancelling;
        private volatile LinkedBlockingQueue<String> mmQueue;

        public ELMCommandQueueThread() {
            if (D) Log.d(TAG, "ELMCommandQueueThread.ELMCommandQueueThread()");

            mmQueue = new LinkedBlockingQueue<String>();
        }

        @Override
        public void run() {
            if (D) Log.d(TAG, "ELMCommandQueueThread.run()");

            try {
                String command;
                while (!mmCancelling) {
                    if (mBTState != CBIServiceMain.BTState.IDLE) {
                        //socket is busy with a TX or RX
                        SystemClock.sleep(CBIServiceMain.ELM_COMMAND_QUEUE_BUSY_WAIT_TIME);
                    } else {
                        command = mmQueue.take();
                        CBIServiceMain.this.elmSendCommand(command);
                    }
                }
            } catch (Exception e) {
                Log.w(TAG, "ELMCommandQueueThread.run() : exception while processing queue : exception= " + e.getMessage(), e);
            }
        }

        public void add(final String command) {
            if (D) Log.d(TAG, "ELMCommandQueueThread.add() : command= " + command);

            if (mmCancelling) {
                return;
            }

            try {
                mmQueue.put(command);
            } catch (Exception e) {
                Log.w(TAG, "ELMCommandQueueThread.add() : failed to add to queue : exception= " + e.getMessage(), e);
            }
        }

        public void cancel() {
            if (D) Log.d(TAG, "ELMCommandQueueThread.cancel()");

            mmCancelling = true;
        }
    }
}