package org.apache.cordova.bluetooth;

import java.util.ArrayList;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;

import org.apache.cordova.CordovaWebView;
import org.apache.cordova.api.PluginResult;
import org.apache.cordova.api.CordovaPlugin;
import org.apache.cordova.api.CallbackContext;
import org.apache.cordova.api.CordovaInterface;

import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONException;

import android.util.Log;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.annotation.TargetApi;


/**
 * Bluetooth interface for Cordova 2.6.0 (PhoneGap).
 * 
 * @version 	0.9.1
 * @author  	Taneli Hartikainen
 */
@TargetApi(Build.VERSION_CODES.GINGERBREAD)
public class BluetoothPlugin extends CordovaPlugin 
{	
	private static final String LOG_TAG					= "BluetoothPlugin";
	
	private static final String ACTION_IS_BT_ENABLED 	= "isEnabled";
	private static final String ACTION_ENABLE_BT		= "enable";
	private static final String ACTION_DISABLE_BT		= "disable";

	private static final String ACTION_IS_DISCOVERING	= "isDiscovering";
	private static final String ACTION_START_DISCOVERY	= "startDiscovery";
	private static final String ACTION_STOP_DISCOVERY	= "stopDiscovery";
	
	private static final String ACTION_IS_PAIRED		= "isPaired";
	private static final String ACTION_PAIR				= "pair";
	private static final String ACTION_UNPAIR			= "unpair";
	
	private static final String ACTION_GET_PAIRED		= "getPaired";
	private static final String ACTION_GET_UUIDS		= "getUuids";
	
	private static final String ACTION_IS_CONNECTED		= "isConnected";
	private static final String ACTION_IS_READING		= "isConnectionManaged";
	private static final String ACTION_CONNECT 			= "connect";
	private static final String ACTION_DISCONNECT 		= "disconnect";
	
	private	static final String ACTION_START_READING	= "startConnectionManager";
	private	static final String ACTION_STOP_READING		= "stopConnectionManager";
	
	private static final String ACTION_WRITE			= "write";

	/**
	 * Bluetooth interface
	 */
	private BluetoothWrapper _bluetooth;
	
	/**
	 * Callback context for device discovery actions.
	 */
	private CallbackContext _discoveryCallback;
	
	/**
	 * Callback context for pairing devices.
	 */
	private CallbackContext _pairingCallback;
	
	/**
	 * Callback context for fetching UUIDs.
	 */
	private CallbackContext	_uuidCallback;
	
	/**
	 * Callback context for the asynchronous connection attempt.
	 */
	private CallbackContext _connectCallback;
	
	/**
	 * Callback context for the asynchronous (and continuous) read operation.
	 */
	private CallbackContext _ioCallback;
	
	/**
	 * Is set to true when a discovery process is canceled or a new one is started when
	 * there is a discovery process still in progress (cancels the old one).
	 */
	private boolean _wasDiscoveryCanceled;
	
	
	/**
	 * Initialize the Plugin, Cordova handles this.
	 * 
	 * @param cordova	Used to get register Handler with the Context accessible from this interface 
	 * @param view		Passed straight to super's initialization.
	 */
	public void initialize(CordovaInterface cordova, CordovaWebView view)
	{
		super.initialize(cordova, view);

		_bluetooth = new BluetoothWrapper(cordova.getActivity().getBaseContext(), _handler);
		_wasDiscoveryCanceled = false;
	}

	/**
	 * Executes the given action.
	 * 
	 * @param action		The action to execute.
	 * @param args			Potential arguments.
	 * @param callbackCtx	Babby call home.
	 */
	@Override
	public boolean execute(String action, JSONArray args, CallbackContext callbackCtx)
	{	
		if(ACTION_IS_BT_ENABLED.equals(action))
		{
			isEnabled(args, callbackCtx);
		}
		else if(ACTION_ENABLE_BT.equals(action))
		{
			enable(args, callbackCtx);
		}
		else if(ACTION_DISABLE_BT.equals(action))
		{
			disable(args, callbackCtx);
		}
		else if(ACTION_IS_DISCOVERING.equals(action))
		{
			isDiscovering(args, callbackCtx);
		}
		else if(ACTION_START_DISCOVERY.equals(action))
		{
			startDiscovery(args, callbackCtx);
		}
		else if(ACTION_STOP_DISCOVERY.equals(action))
		{
			stopDiscovery(args, callbackCtx);
		}
		else if(ACTION_IS_PAIRED.equals(action))
		{
			isPaired(args, callbackCtx);
		}
		else if(ACTION_PAIR.equals(action))
		{
			pair(args, callbackCtx);
		}
		else if(ACTION_UNPAIR.equals(action))
		{
			unpair(args, callbackCtx);
		}
		else if(ACTION_GET_PAIRED.equals(action))
		{
			getPaired(args, callbackCtx);
		}
		else if(ACTION_GET_UUIDS.equals(action))
		{
			getUuids(args, callbackCtx);
		}
		else if(ACTION_IS_CONNECTED.equals(action))
		{
			isConnected(args, callbackCtx);
		}
		else if(ACTION_CONNECT.equals(action))
		{
			connect(args, callbackCtx);
		}
		else if(ACTION_DISCONNECT.equals(action))
		{
			disconnect(args, callbackCtx);
		}
		else if(ACTION_IS_READING.equals(action))
		{
			isConnectionManaged(args, callbackCtx);
		}
		else if(ACTION_START_READING.equals(action))
		{
			startConnectionManager(args, callbackCtx);
		}
		else if(ACTION_STOP_READING.equals(action))
		{
			stopConnectionManager(args, callbackCtx);
		}
		else if(ACTION_WRITE.equals(action))
		{
			write(args, callbackCtx);
		}
		else
		{
			Log.e(LOG_TAG, "Invalid Action[" + action + "]");
			callbackCtx.sendPluginResult(new PluginResult(PluginResult.Status.INVALID_ACTION));
		}
		
		return true;
	}
	
	/**
	 * Send an error to given CallbackContext containing the error code and message.
	 * 
	 * @param ctx	Where to send the error.
	 * @param msg	What seems to be the problem.
	 * @param code	Integer value as a an error "code"
	 */
	private void error(CallbackContext ctx, String msg, int code)
	{
		try
		{
			JSONObject result = new JSONObject();
			result.put("message", msg);
			result.put("code", code);
			
			ctx.error(result);
		}
		catch(Exception e)
		{
			Log.e(LOG_TAG, "Error with... error raising, " + e.getMessage());
		}
	}
	
	/**
	 * Is Bluetooth on.
	 * 
	 * @param args			Arguments given.
	 * @param callbackCtx	Where to send results.
	 */
	private void isEnabled(JSONArray args, CallbackContext callbackCtx)
	{
		try 
		{
			callbackCtx.sendPluginResult(new PluginResult(PluginResult.Status.OK, _bluetooth.isEnabled()));
		} 
		catch(Exception e) 
		{
			this.error(callbackCtx, e.getMessage(), BluetoothError.ERR_UNKNOWN);
		}
	}
	
	/**
	 * Turn Bluetooth on.
	 * 
	 * @param args			Arguments given.
	 * @param callbackCtx	Where to send results.
	 */
	private void enable(JSONArray args, CallbackContext callbackCtx)
	{
		// TODO Add options to enable with Intent
		
		try
		{
			_bluetooth.enable();
			callbackCtx.success();
		}
		catch(Exception e)
		{
			this.error(callbackCtx, e.getMessage(), BluetoothError.ERR_UNKNOWN);
		}
	}

	/**
	 * Turn Bluetooth off.
	 * 
	 * @param args			Arguments given.
	 * @param callbackCtx	Where to send results.
	 */
	private void disable(JSONArray args, CallbackContext callbackCtx)
	{
		try
		{
			_bluetooth.disable();
			callbackCtx.success();
		}
		catch(Exception e)
		{
			this.error(callbackCtx, e.getMessage(), BluetoothError.ERR_UNKNOWN);
		}
	}

	/**
	 * See if a device discovery process is in progress.
	 * 
	 * @param args			Arguments given.
	 * @param callbackCtx	Where to send results.
	 */
	private void isDiscovering(JSONArray args, CallbackContext callbackCtx)
	{
		try 
		{
			callbackCtx.sendPluginResult(new PluginResult(PluginResult.Status.OK, _bluetooth.isDiscovering()));
		} 
		catch(Exception e) 
		{
			this.error(callbackCtx, e.getMessage(), BluetoothError.ERR_UNKNOWN);
		}
	}

	/**
	 * Start a device discovery.
	 * 
	 * @param args			Arguments given.
	 * @param callbackCtx	Where to send results.
	 */
	private void startDiscovery(JSONArray args, CallbackContext callbackCtx)
	{
		// TODO Someday add an option to fetch UUIDs at the same time
		
		try
		{
			if(_bluetooth.isConnecting())
			{
				this.error(callbackCtx, "A Connection attempt is in progress.", BluetoothError.ERR_CONNECTING_IN_PROGRESS);
			}
			else
			{
				if(_bluetooth.isDiscovering())
				{
					_wasDiscoveryCanceled = true;
					_bluetooth.stopDiscovery();
					
					if(_discoveryCallback != null)
					{
						this.error(_discoveryCallback, 
							"Discovery was stopped because a new discovery was started.", 
							BluetoothError.ERR_DISCOVERY_RESTARTED
						);
						_discoveryCallback = null;
					}
				}
				
				_bluetooth.startDiscovery();
				
				PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT);
				result.setKeepCallback(true);
				callbackCtx.sendPluginResult(result);
	
				_discoveryCallback = callbackCtx;
			}
		}
		catch(Exception e)
		{
			this.error(callbackCtx, e.getMessage(), BluetoothError.ERR_UNKNOWN);
		}
	}

	/**
	 * Stop device discovery.
	 * 
	 * @param args			Arguments given.
	 * @param callbackCtx	Where to send results.
	 */
	private void stopDiscovery(JSONArray args, CallbackContext callbackCtx)
	{
		try
		{
			if(_bluetooth.isDiscovering())
			{
				_wasDiscoveryCanceled = true;
				_bluetooth.stopDiscovery();
				
				if(_discoveryCallback != null)
				{
					this.error(_discoveryCallback, 
						"Discovery was cancelled.", 
						BluetoothError.ERR_DISCOVERY_CANCELED
					);
					
					_discoveryCallback = null;
				}
				
				callbackCtx.success();
			}
			else
			{
				this.error(callbackCtx, "There is no discovery to cancel.", BluetoothError.ERR_UNKNOWN);
			}
		}
		catch(Exception e)
		{
			this.error(callbackCtx, e.getMessage(), BluetoothError.ERR_UNKNOWN);
		}
	}

	/**
	 * See if the device is paired with the device in the given address.
	 * 
	 * @param args			Arguments given. First argument should be the address in String format.
	 * @param callbackCtx	Where to send results.
	 */
	private void isPaired(JSONArray args, CallbackContext callbackCtx)
	{
		try
		{
			String address = args.getString(0);
			callbackCtx.sendPluginResult(new PluginResult(PluginResult.Status.OK, _bluetooth.isBonded(address)));
		}
		catch(Exception e)
		{
			this.error(callbackCtx, e.getMessage(), BluetoothError.ERR_UNKNOWN);
		}
	}

	/**
	 * Pair the device with the device in the given address.
	 * 
	 * @param args			Arguments given. First argument should be the address in String format.
	 * @param callbackCtx	Where to send results.
	 */
	private void pair(JSONArray args, CallbackContext callbackCtx)
	{
		// TODO Add a timeout function for pairing
		
		if(_pairingCallback != null)
		{
			this.error(callbackCtx, "Pairing process is already in progress.", BluetoothError.ERR_PAIRING_IN_PROGRESS);
		}
		else
		{
			try
			{
				String address = args.getString(0);
				_bluetooth.createBond(address);
				_pairingCallback = callbackCtx;
			}
			catch(Exception e)
			{
				_pairingCallback = null;
				this.error(callbackCtx, e.getMessage(), BluetoothError.ERR_UNKNOWN);
			}
		}
	}

	/**
	 * Unpair with the device in the given address.
	 * 
	 * @param args			Arguments given. First argument should be the address in String format.
	 * @param callbackCtx	Where to send results.
	 */
	private void unpair(JSONArray args, CallbackContext callbackCtx)
	{
		try
		{
			String address = args.getString(0);
			_bluetooth.removeBond(address);
			callbackCtx.success();
		}
		catch(Exception e)
		{
			this.error(callbackCtx, e.getMessage(), BluetoothError.ERR_UNKNOWN);
		}
	}
	

	/**
	 * Get the devices paired with this device.
	 * 
	 * @param args			Arguments given.
	 * @param callbackCtx	Where to send results.
	 */
	private void getPaired(JSONArray args, CallbackContext callbackCtx)
	{
		try
		{
			JSONArray devices 						= new JSONArray();
			ArrayList<Pair<String>> bondedDevices 	= _bluetooth.getBondedDevices();
			
			for(Pair<String> deviceInfo : bondedDevices)
			{
				JSONObject device = new JSONObject();
				device.put("name", deviceInfo.a);
				device.put("address", deviceInfo.b);
				devices.put(device);
			}
			
			callbackCtx.success(devices);
		}
		catch(Exception e)
		{
			this.error(callbackCtx, e.getMessage(), BluetoothError.ERR_UNKNOWN);
		}
	}

	/**
	 * Get the UUID(s) of the device at given address.
	 * 
	 * @param args			Arguments given. First argument should be the address in String format.
	 * @param callbackCtx	Where to send results.
	 */
	private void getUuids(JSONArray args, CallbackContext callbackCtx)
	{
		if(_uuidCallback != null)
		{
			this.error(callbackCtx, 
				"Could not start UUID fetching because there is already one in progress.", 
				BluetoothError.ERR_UUID_FETCHING_IN_PROGRESS
			);
		}
		else
		{
			try
			{
				String address = args.getString(0);
				_bluetooth.fetchUuids(address);
				_uuidCallback = callbackCtx;
				
			}
			catch(Exception e)
			{
				_uuidCallback = null;
				this.error(callbackCtx, e.getMessage(), BluetoothError.ERR_UNKNOWN);
			}
		}
	}

	/**
	 * See if we have a connection.
	 * 
	 * @param args			Arguments given.
	 * @param callbackCtx	Where to send results.
	 */
	private void isConnected(JSONArray args, CallbackContext callbackCtx)
	{
		try 
		{	
			callbackCtx.sendPluginResult(new PluginResult(PluginResult.Status.OK, _bluetooth.isConnected()));
		} 
		catch(Exception e)
		{
			this.error(callbackCtx, e.getMessage(), BluetoothError.ERR_UNKNOWN);
		}
	}

	/**
	 * Attempt to connect to a device.
	 * 
	 * @param args			Arguments given. [Address, UUID, ConnectionType(Secure, Insecure, Hax)], String format.
	 * @param callbackCtx	Where to send results.
	 */
	private void connect(JSONArray args, CallbackContext callbackCtx)
	{
		boolean isConnecting 	= _bluetooth.isConnecting();
		boolean isConnected		= _bluetooth.isConnected(); 
		
		if(isConnecting)
		{
			this.error(callbackCtx, "There is already a connection attempt in progress.", BluetoothError.ERR_CONNECTING_IN_PROGRESS);
		}
		else if(isConnected)
		{
			this.error(callbackCtx, "There is already a connection in progress.", BluetoothError.ERR_CONNECTION_ALREADY_EXISTS);
		}
		else
		{
			try
			{
				if(_bluetooth.isDiscovering())
				{
					_wasDiscoveryCanceled = true;
					_bluetooth.stopDiscovery();
					
					if(_discoveryCallback != null)
					{
						this.error(_discoveryCallback, "Discovery stopped because a connection attempt was started.", BluetoothError.ERR_DISCOVERY_CANCELED);
					}
				}
			
				String address 		= args.getString(0);
				String uuid			= args.getString(1);
				String connTypeStr	= args.getString(2);
				
				_bluetooth.connect(address, uuid, connTypeStr);
				
				PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT);
				result.setKeepCallback(true);
				callbackCtx.sendPluginResult(result);
				
				_connectCallback = callbackCtx;
			}
			catch(Exception e)
			{
				_connectCallback = null;
				
				this.error(callbackCtx, e.getMessage(), BluetoothError.ERR_UNKNOWN);
			}
		}
	}

	/**
	 * Disconnect from the device currently connected to.
	 * 
	 * @param args			Arguments given.
	 * @param callbackCtx	Where to send results.
	 */
	private void disconnect(JSONArray args, CallbackContext callbackCtx)
	{
		try 
		{
			_bluetooth.disconnect();
			callbackCtx.success();
		} 
		catch(Exception e) 
		{
			this.error(callbackCtx, e.getMessage(), BluetoothError.ERR_UNKNOWN);
		}
	}

	/**
	 * See if we have a managed connection active (allows read/write).
	 * 
	 * @param args			Arguments given.
	 * @param callbackCtx	Where to send results.
	 */
	private void isConnectionManaged(JSONArray args, CallbackContext callbackCtx)
	{
		callbackCtx.sendPluginResult(new PluginResult(PluginResult.Status.OK, _bluetooth.isConnectionManaged()));
	}

	/**
	 * Start a managed connection, allowing read and write operations.
	 * 
	 * @param args			Arguments given.
	 * @param callbackCtx	Where to send results.
	 */
	private void startConnectionManager(JSONArray args, CallbackContext callbackCtx)
	{
		if(_ioCallback != null)
		{
			this.error(callbackCtx, "There is already an active connection.", BluetoothError.ERR_CONNECTION_ALREADY_EXISTS);
		}
		else
		{
			try
			{
				_bluetooth.startConnectionManager();
				_ioCallback = callbackCtx;
			}
			catch(Exception e)
			{
				_ioCallback = null;
				this.error(callbackCtx, e.getMessage(), BluetoothError.ERR_UNKNOWN);
			}
		}
	}
	
	/**
	 * Stop the managed connection, preventing further read or write operations.
	 * 
	 * @param args			Arguments given.
	 * @param callbackCtx	Where to send results.
	 */
	private void stopConnectionManager(JSONArray args, CallbackContext callbackCtx)
	{
		try
		{
			if(_bluetooth.isConnectionManaged())
			{
				_bluetooth.stopConnectionManager();
				callbackCtx.success();
			}
			else
			{
				this.error(callbackCtx, 
					"There is no connection being managed.", 
					BluetoothError.ERR_CONNECTION_DOESNT_EXIST
				);
			}
		}
		catch(Exception e)
		{
			this.error(callbackCtx, e.getMessage(), BluetoothError.ERR_UNKNOWN);
		}
	}

	/**
	 * Write given data to the managed connection.
	 * 
	 * @param args			Arguments given. First argument should be the data you want to write.
	 * @param callbackCtx	Where to send results.
	 */
	private void write(JSONArray args, CallbackContext callbackCtx)
	{
		Log.d(LOG_TAG, "write-method called");
		
		try 
		{
			Object data 		= args.get(0);
			String encoding 	= args.getString(1);
			boolean forceString = args.getBoolean(2); 
			
			byte[] defaultBytes = new byte[4];
			ByteBuffer buffer = ByteBuffer.wrap(defaultBytes);
			
			if(forceString || data.getClass() == String.class)
			{
				String dataString = (String)data;
				buffer = ByteBuffer.wrap(dataString.getBytes(encoding));
			}
			else if(data.getClass().equals(Integer.class))
			{	
				byte[] bytes = new byte[4];
				buffer = ByteBuffer.wrap(bytes);
				buffer.putInt((Integer)data);
			}
			else if(data.getClass().equals(Double.class))
			{
				byte[] bytes = new byte[8];
				buffer = ByteBuffer.wrap(bytes);
				buffer.putDouble((Double)data);
			}
			else
			{
				this.error(callbackCtx, "Unknown data-type", BluetoothError.ERR_UNKNOWN);
				return;
			}
			
			if(!_bluetooth.isConnected())
			{
				this.error(callbackCtx, "There is no managed connection to write to.", BluetoothError.ERR_CONNECTION_DOESNT_EXIST);
			}
			else
			{
				buffer.rewind();
				_bluetooth.write(buffer.array());
				callbackCtx.success();
			}
		} 
		catch (Exception e) 
		{
			this.error(callbackCtx, e.getMessage(), BluetoothError.ERR_UNKNOWN);
		}
	}
	
	/**
	 * Handle messages from BluetoothWrapper. BluetoothWrapper does a lot of asynchronous
	 * work, so the main way of communicating between BluetoothPlugin and BluetoothWrapper 
	 * is to use callback Messages.
	 * 
	 * @see Handler
	 * @see Message
	 * @see BluetoothWrapper
	 */
	private final Handler _handler = new Handler(new Handler.Callback() 
	{	
		@Override
		public boolean handleMessage(Message msg) 
		{	
			switch(msg.what)
			{
				case BluetoothWrapper.MSG_DISCOVERY_STARTED:
					
					_wasDiscoveryCanceled = false;
					
					break;
					
				case BluetoothWrapper.MSG_DISCOVERY_FINISHED:
					
					if(!_wasDiscoveryCanceled)
					{
						if(_discoveryCallback != null)
						{
							PluginResult result = new PluginResult(PluginResult.Status.OK, false);
							_discoveryCallback.sendPluginResult(result);
							_discoveryCallback = null;
						}
					}
					
					break;

				case BluetoothWrapper.MSG_DEVICE_FOUND:
					
					try
					{
						String name 	= msg.getData().getString(BluetoothWrapper.DATA_DEVICE_NAME);
						String address 	= msg.getData().getString(BluetoothWrapper.DATA_DEVICE_ADDRESS);
						
						JSONObject device = new JSONObject();
						device.put("name", name);
						device.put("address", address);
						
						// Send one device at a time, keeping callback to be used again
						if(_discoveryCallback != null)
						{
							PluginResult result = new PluginResult(PluginResult.Status.OK, device);
							result.setKeepCallback(true);
							_discoveryCallback.sendPluginResult(result);
						}
						else
						{
							Log.e(LOG_TAG, "CallbackContext for discovery doesn't exist.");
						}
					}
					catch(JSONException e)
					{
						if(_discoveryCallback != null)
						{
							BluetoothPlugin.this.error(_discoveryCallback,
								e.getMessage(),
								BluetoothError.ERR_UNKNOWN
							);
							_discoveryCallback = null;
						}
					}

					break;

				case BluetoothWrapper.MSG_UUIDS_FOUND:
					
					try
					{
						if(_uuidCallback != null)
						{
							String name 			= msg.getData().getString(BluetoothWrapper.DATA_DEVICE_NAME);
							String address 			= msg.getData().getString(BluetoothWrapper.DATA_DEVICE_ADDRESS);
							ArrayList<String> uuids = msg.getData().getStringArrayList(BluetoothWrapper.DATA_UUIDS);
							
							JSONObject deviceInfo = new JSONObject();
							JSONArray deviceUuids = new JSONArray(uuids);
							
							deviceInfo.put("name", name);
							deviceInfo.put("address", address);
							deviceInfo.put("uuids", deviceUuids);
							
							_uuidCallback.success(deviceInfo);
							_uuidCallback = null;
						}
						else
						{
							Log.e(LOG_TAG, "CallbackContext for uuid fetching doesn't exist.");
						}	
					}
					catch(Exception e)
					{
						if(_uuidCallback != null)
						{
							BluetoothPlugin.this.error(_uuidCallback,
								e.getMessage(), BluetoothError.ERR_UNKNOWN
							);
							_uuidCallback = null;
						}
					}
					
					break;
					
				case BluetoothWrapper.MSG_DEVICE_BONDED:
					
					try
					{
						String name 	= msg.getData().getString(BluetoothWrapper.DATA_DEVICE_NAME);
						String address 	= msg.getData().getString(BluetoothWrapper.DATA_DEVICE_ADDRESS);
						
						JSONObject bondedDevice = new JSONObject();
						bondedDevice.put("name", name);
						bondedDevice.put("address", address);
						
						if(_pairingCallback != null)
						{
							_pairingCallback.success(bondedDevice);
							_pairingCallback = null;
						}
						else
						{
							Log.e(LOG_TAG, "CallbackContext for pairing doesn't exist.");
						}	
					}
					catch(Exception e)
					{
						if(_pairingCallback != null)
						{
							BluetoothPlugin.this.error(_pairingCallback, 
								e.getMessage(), BluetoothError.ERR_PAIRING_FAILED
							);
							_pairingCallback = null;
						}
					}
					
					break;
					
				case BluetoothWrapper.MSG_CONNECTION_ESTABLISHED:
					
					if(_connectCallback != null)
					{
						_connectCallback.success();
						_connectCallback = null;
					}
					else
					{
						Log.e(LOG_TAG, "CallbackContext for connection doesn't exist.");
					}
					
					break;
					
				case BluetoothWrapper.MSG_CONNECTION_FAILED:

					String error = msg.getData().getString(BluetoothWrapper.DATA_ERROR);
					
					if(_connectCallback != null)
					{
						BluetoothPlugin.this.error(_connectCallback, 
							error, BluetoothError.ERR_CONNECTING_FAILED
						);
						_connectCallback = null;
					}
					else
					{
						Log.e(LOG_TAG, "CallbackContext for connection doesn't exist.");
					}
					
					break;
					
				case BluetoothWrapper.MSG_CONNECTION_LOST:
					
					if(_connectCallback != null)
					{
						BluetoothPlugin.this.error(_connectCallback,
							"Connection lost.", BluetoothError.ERR_CONNECTION_LOST
						);
						_connectCallback = null;
					}
					
					if(_ioCallback != null)
					{
						BluetoothPlugin.this.error(_ioCallback,
							"Connection lost.", BluetoothError.ERR_CONNECTION_LOST
						);
						_ioCallback = null;
					}
					
					break;
					
				case BluetoothWrapper.MSG_CONNECTION_STOPPED:
					
					if(_connectCallback != null)
					{
						BluetoothPlugin.this.error(_connectCallback,
							"Disconnected.", BluetoothError.ERR_DISCONNECTED
						);
						_connectCallback = null;
					}
					
					if(_ioCallback != null)
					{
						BluetoothPlugin.this.error(_ioCallback,
							"Disconnected.", BluetoothError.ERR_DISCONNECTED
						);
						_ioCallback = null;
					}
					
					break;
					
				case BluetoothWrapper.MSG_READ:
					
					String data = new String(
						msg.getData().getByteArray(BluetoothWrapper.DATA_BYTES), 
						Charset.forName("UTF-8")
					);
					
					if(_ioCallback != null)
					{
						PluginResult result = new PluginResult(PluginResult.Status.OK, data);
						result.setKeepCallback(true);
						_ioCallback.sendPluginResult(result);
					}
					else
					{
						Log.e(LOG_TAG, "CallbackContext for IO doesn't exist.");
					}
					
					break;
					
				case BluetoothWrapper.MSG_BLUETOOTH_LOST:
					
					if(_discoveryCallback != null)
					{
						BluetoothPlugin.this.error(_discoveryCallback,
							"Bluetooth lost.", BluetoothError.ERR_BLUETOOTH_LOST
						);
						_discoveryCallback = null;
					}

					if(_pairingCallback != null)
					{
						BluetoothPlugin.this.error(_pairingCallback,
							"Bluetooth lost.", BluetoothError.ERR_BLUETOOTH_LOST
						);
						_pairingCallback = null;
					}
					
					if(_uuidCallback != null)
					{
						BluetoothPlugin.this.error(_uuidCallback,
							"Bluetooth lost.", BluetoothError.ERR_BLUETOOTH_LOST
						);
						_uuidCallback = null;
					}
					
					if(_connectCallback != null)
					{
						BluetoothPlugin.this.error(_connectCallback,
							"Bluetooth lost.", BluetoothError.ERR_BLUETOOTH_LOST
						);
						_connectCallback = null;
					}
					
					if(_ioCallback != null)
					{
						BluetoothPlugin.this.error(_ioCallback,
							"Bluetooth lost.", BluetoothError.ERR_BLUETOOTH_LOST
						);
						_ioCallback = null;
					}
					
					break;
					
				default:
					
					Log.e(LOG_TAG, "Message type could not be resolved.");
					
					break;
			}
			
			return true;
		}
	});
}