// API definition for EvoThings BLE plugin.
//
// Use jsdoc to generate documentation.

// The following line causes a jsdoc error.
// Use the jsdoc option -l to ignore the error.
var exec = cordova.require('cordova/exec');

/**
 * @module cordova-plugin-ble
 * @description Functions and properties in this module are available
 * under the global name <code>evothings.ble</code>
 */

/********** BLE Central API **********/

// Flag that tracks if scanning is in progress.
//  Used by startScan and stopScan.
var isScanning = false;

/**
 * Start scanning for devices.
 * <p>An array of service UUID strings may be given in the options object parameter.
 * One or more service UUIDs must be specified for iOS background scanning to work.</p>
 * <p>Found devices and errors are reported to the supplied callback functions.</p>
 * <p>Will keep scanning until you call stopScan().</p>
 * <p>To conserve energy, call stopScan() as soon as you've found the device
 * you're looking for.</p>
 * <p>Call stopScan() before calling startScan() again.</p>
 *
 * @param {scanCallback} success - Success callback, called repeatedly
 * for each found device.
 * @param {failCallback} fail - Error callback.
 * @param {ScanOptions} options - Optional object with options.
 * Set field serviceUUIDs to an array of service UUIDs to scan for.
 * Set field parseAdvertisementData to false to disable automatic
 * parsing of advertisement data.
 *
 * @example
 *   // Scan for all services.
 *   evothings.ble.startScan(
 *       function(device)
 *       {
 *           console.log('startScan found device named: ' + device.name);
 *       },
 *       function(errorCode)
 *       {
 *           console.log('startScan error: ' + errorCode);
 *       }
 *   );
 *
 *   // Scan for specific service (Eddystone Service UUID).
 *   evothings.ble.startScan(
 *       function(device)
 *       {
 *           console.log('startScan found device named: ' + device.name);
 *       },
 *       function(errorCode)
 *       {
 *           console.log('startScan error: ' + errorCode);
 *       },
 *       { serviceUUIDs: ['0000feaa-0000-1000-8000-00805f9b34fb'] }
 *   );
 */
exports.startScan = function(arg1, arg2, arg3, arg4)
{
	// Scanning parameters.
	var serviceUUIDs;
	var success;
	var fail;
	var options;
	var parseAdvertisementData = true;

	function onFail(error)
	{
		isScanning = false;
		fail(error);
	}

	function onSuccess(device)
	{
		// Only report results while scanning is requested.
		if (isScanning)
		{
			if (parseAdvertisementData)
			{
				exports.parseAdvertisementData(device);
			}
			success(device);
		}
	}

	// Determine parameters.
	if (Array.isArray(arg1))
	{
		// First param is an array of serviceUUIDs.
		serviceUUIDs = arg1;
		success = arg2;
		fail = arg3;
		options = arg4;
	}
	else if ('function' == typeof arg1)
	{
		// First param is a function.
		serviceUUIDs = null;
		success = arg1;
		fail = arg2;
		options = arg3;
	}

	if (isScanning)
	{
		fail('Scan already in progress');
		return;
	}

	isScanning = true;

	// Set options.
	if (options)
	{
		if (Array.isArray(options.serviceUUIDs))
		{
			serviceUUIDs = options.serviceUUIDs;
		}

		if (options.parseAdvertisementData === true)
		{
			parseAdvertisementData = true;
		}
		else if (options.parseAdvertisementData === false)
		{
			parseAdvertisementData = false;
		}
	}

	// Start scanning.
	isScanning = true;
	if (Array.isArray(serviceUUIDs))
	{
		serviceUUIDs = getCanonicalUUIDArray(serviceUUIDs);
		exec(onSuccess, onFail, 'BLE', 'startScan', [serviceUUIDs]);
	}
	else
	{
		exec(onSuccess, onFail, 'BLE', 'startScan', []);
	}
};

/**
 * Ensure that all UUIDs in an array has canonical form.
 * @private
 */
function getCanonicalUUIDArray(uuidArray)
{
	var result = [];
	for (var i in uuidArray)
	{
		result.push(exports.getCanonicalUUID(uuidArray[i]));
	}
}

/**
 * Options for startScan.
 * @typedef {Object} ScanOptions
 * @param {array} serviceUUIDs - Array with service UUID strings (optional).
 * On iOS multiple UUIDs are scanned for using logical OR operator,
 * any UUID that matches any of the UUIDs adverticed by the device
 * will count as a match. On Android, multiple UUIDs are scanned for
 * using AND logic, the device must advertise all of the given UUIDs
 * to produce a match. (The matching logic will be unified in future
 * versions of the plugin.) When providing one service UUID, behaviour
 * is the same on Android and iOS. Learning out this parameter or
 * setting it to null, will scan for all devices, regardless of
 * advertised services.
 * @property {boolean} parseAdvertisementData - Set to false to disable
 * automatic parsing of advertisement data from the scan record.
 * Default is true.
 */

/**
 * This function is a parameter to startScan() and is called when a new device is discovered.
 * @callback scanCallback
 * @param {DeviceInfo} device
 */

/**
 * Info about a BLE device.
 * @typedef {Object} DeviceInfo
 * @property {string} address - Uniquely identifies the device.
 * Pass this to connect().
 * The form of the address depends on the host platform.
 * @property {number} rssi - A negative integer, the signal strength in decibels.
 * @property {string} name - The device's name, or nil.
 * @property {string} scanRecord - Base64-encoded binary data.
 * Its meaning is device-specific. Not available on iOS.
 * @property {AdvertisementData} advertisementData - Object containing some
 * of the data from the scanRecord. Available natively on iOS. Available on
 * Android by parsing the scanRecord, which is implemented in the library EasyBLE:
 * {@link https://github.com/evothings/evothings-libraries/blob/master/libs/evothings/easyble/easyble.js}.
 */

/**
 * Information extracted from a scanRecord. Some or all of the fields may
 * be undefined. This varies between BLE devices.
 * Depending on OS version and BLE device, additional fields, not documented
 * here, may be present.
 * @typedef {Object} AdvertisementData
 * @property {string} kCBAdvDataLocalName - The device's name. Might or might
 * not be equal to DeviceInfo.name. iOS caches DeviceInfo.name which means if
 * the name is changed on the device, the new name might not be visible.
 * kCBAdvDataLocalName is not cached and is therefore safer to use, when available.
 * @property {number} kCBAdvDataTxPowerLevel - Transmission power level as
 * advertised by the device.
 * @property {number} kCBAdvDataChannel - A positive integer, the BLE channel
 * on which the device listens for connections. Ignore this number.
 * @property {boolean} kCBAdvDataIsConnectable - True if the device accepts
 * connections. False if it doesn't.
 * @property {array} kCBAdvDataServiceUUIDs - Array of strings, the UUIDs of
 * services advertised by the device. Formatted according to RFC 4122, all lowercase.
 * @property {object} kCBAdvDataServiceData - Dictionary of strings to strings.
 * The keys are service UUIDs. The values are base-64-encoded binary data.
 * @property {string} kCBAdvDataManufacturerData - Base-64-encoded binary data.
 * This field is used by BLE devices to advertise custom data that don't fit into
 * any of the other fields.
 */

/**
 * This function is called when an operation fails.
 * @callback failCallback
 * @param {string} errorString - A human-readable string that describes the error that occurred.
 */

/**
 * Stops scanning for devices.
 *
 * @example
 *   evothings.ble.stopScan();
 */
exports.stopScan = function()
{
	isScanning = false;
	exec(null, null, 'BLE', 'stopScan', []);
};

// Create closure for parseAdvertisementData and helper functions.
// TODO: Investigate if the code can be simplified, compare to how
// how the Evothings Bleat implementation does this.
;(function()
{
var base64;

/**
 * Parse the advertisement data in the scan record.
 * If device already has AdvertisementData, does nothing.
 * If device instead has scanRecord, creates AdvertisementData.
 * See  {@link AdvertisementData} for reference documentation.
 * @param {DeviceInfo} device - Device object.
 */
exports.parseAdvertisementData = function(device)
{
	if (!base64) { base64 = cordova.require('cordova/base64'); }

	// If device object already has advertisementData we
	// do not need to parse the scanRecord.
	if (device.advertisementData) { return; }

	// Must have scanRecord yo continue.
	if (!device.scanRecord) { return; }

	// Here we parse BLE/GAP Scan Response Data.
	// See the Bluetooth Specification, v4.0, Volume 3, Part C, Section 11,
	// for details.

	var byteArray = base64DecToArr(device.scanRecord);
	var pos = 0;
	var advertisementData = {};
	var serviceUUIDs;
	var serviceData;

	// The scan record is a list of structures.
	// Each structure has a length byte, a type byte, and (length-1) data bytes.
	// The format of the data bytes depends on the type.
	// Malformed scanRecords will likely cause an exception in this function.
	while (pos < byteArray.length)
	{
		var length = byteArray[pos++];
		if (length == 0)
		{
			break;
		}
		length -= 1;
		var type = byteArray[pos++];

		// Parse types we know and care about.
		// Skip other types.

		var BLUETOOTH_BASE_UUID = '-0000-1000-8000-00805f9b34fb'

		// Convert 16-byte Uint8Array to RFC-4122-formatted UUID.
		function arrayToUUID(array, offset)
		{
			var k=0;
			var string = '';
			var UUID_format = [4, 2, 2, 2, 6];
			for (var l=0; l<UUID_format.length; l++)
			{
				if (l != 0)
				{
					string += '-';
				}
				for (var j=0; j<UUID_format[l]; j++, k++)
				{
					string += toHexString(array[offset+k], 1);
				}
			}
			return string;
		}

		if (type == 0x02 || type == 0x03) // 16-bit Service Class UUIDs.
		{
			serviceUUIDs = serviceUUIDs ? serviceUUIDs : [];
			for(var i=0; i<length; i+=2)
			{
				serviceUUIDs.push(
					'0000' +
					toHexString(
						littleEndianToUint16(byteArray, pos + i),
						2) +
					BLUETOOTH_BASE_UUID);
			}
		}
		if (type == 0x04 || type == 0x05) // 32-bit Service Class UUIDs.
		{
			serviceUUIDs = serviceUUIDs ? serviceUUIDs : [];
			for (var i=0; i<length; i+=4)
			{
				serviceUUIDs.push(
					toHexString(
						littleEndianToUint32(byteArray, pos + i),
						4) +
					BLUETOOTH_BASE_UUID);
			}
		}
		if (type == 0x06 || type == 0x07) // 128-bit Service Class UUIDs.
		{
			serviceUUIDs = serviceUUIDs ? serviceUUIDs : [];
			for (var i=0; i<length; i+=16)
			{
				serviceUUIDs.push(arrayToUUID(byteArray, pos + i));
			}
		}
		if (type == 0x08 || type == 0x09) // Local Name.
		{
			advertisementData.kCBAdvDataLocalName = evothings.ble.fromUtf8(
				new Uint8Array(byteArray.buffer, pos, length));
		}
		if (type == 0x0a) // TX Power Level.
		{
			advertisementData.kCBAdvDataTxPowerLevel =
				littleEndianToInt8(byteArray, pos);
		}
		if (type == 0x16) // Service Data, 16-bit UUID.
		{
			serviceData = serviceData ? serviceData : {};
			var uuid =
				'0000' +
				toHexString(
					littleEndianToUint16(byteArray, pos),
					2) +
				BLUETOOTH_BASE_UUID;
			var data = new Uint8Array(byteArray.buffer, pos+2, length-2);
			serviceData[uuid] = base64.fromArrayBuffer(data);
		}
		if (type == 0x20) // Service Data, 32-bit UUID.
		{
			serviceData = serviceData ? serviceData : {};
			var uuid =
				toHexString(
					littleEndianToUint32(byteArray, pos),
					4) +
				BLUETOOTH_BASE_UUID;
			var data = new Uint8Array(byteArray.buffer, pos+4, length-4);
			serviceData[uuid] = base64.fromArrayBuffer(data);
		}
		if (type == 0x21) // Service Data, 128-bit UUID.
		{
			serviceData = serviceData ? serviceData : {};
			var uuid = arrayToUUID(byteArray, pos);
			var data = new Uint8Array(byteArray.buffer, pos+16, length-16);
			serviceData[uuid] = base64.fromArrayBuffer(data);
		}
		if (type == 0xff) // Manufacturer-specific Data.
		{
			// Annoying to have to transform base64 back and forth,
			// but it has to be done in order to maintain the API.
			advertisementData.kCBAdvDataManufacturerData =
				base64.fromArrayBuffer(new Uint8Array(byteArray.buffer, pos, length));
		}

		pos += length;
	}
	advertisementData.kCBAdvDataServiceUUIDs = serviceUUIDs;
	advertisementData.kCBAdvDataServiceData = serviceData;
	device.advertisementData = advertisementData;

	/*
	// Log raw data for debugging purposes.

	console.log("scanRecord: "+evothings.util.typedArrayToHexString(byteArray));

	console.log(JSON.stringify(advertisementData));
	*/
};

/**
 * Decodes a Base64 string. Returns a Uint8Array.
 * nBlocksSize is optional.
 * @param {String} sBase64
 * @param {int} nBlocksSize
 * @return {Uint8Array}
 * @public
 */
function base64DecToArr(sBase64, nBlocksSize) {
	var sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, "");
	var nInLen = sB64Enc.length;
	var nOutLen = nBlocksSize ?
		Math.ceil((nInLen * 3 + 1 >> 2) / nBlocksSize) * nBlocksSize
		: nInLen * 3 + 1 >> 2;
	var taBytes = new Uint8Array(nOutLen);

	for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) {
		nMod4 = nInIdx & 3;
		nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4;
		if (nMod4 === 3 || nInLen - nInIdx === 1) {
			for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) {
				taBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255;
			}
			nUint24 = 0;
		}
	}

	return taBytes;
}

/**
 * Converts a single Base64 character to a 6-bit integer.
 * @private
 */
function b64ToUint6(nChr) {
	return nChr > 64 && nChr < 91 ?
			nChr - 65
		: nChr > 96 && nChr < 123 ?
			nChr - 71
		: nChr > 47 && nChr < 58 ?
			nChr + 4
		: nChr === 43 ?
			62
		: nChr === 47 ?
			63
		:
			0;
}

/**
 * Returns the integer i in hexadecimal string form,
 * with leading zeroes, such that
 * the resulting string is at least byteCount*2 characters long.
 * @param {int} i
 * @param {int} byteCount
 * @public
 */
function toHexString(i, byteCount) {
	var string = (new Number(i)).toString(16);
	while(string.length < byteCount*2) {
		string = '0'+string;
	}
	return string;
}

/**
 * Interpret byte buffer as unsigned little endian 16 bit integer.
 * Returns converted number.
 * @param {ArrayBuffer} data - Input buffer.
 * @param {number} offset - Start of data.
 * @return Converted number.
 * @public
 */
function littleEndianToUint16(data, offset)
{
	return (littleEndianToUint8(data, offset + 1) << 8) +
		littleEndianToUint8(data, offset)
}

/**
 * Interpret byte buffer as unsigned little endian 32 bit integer.
 * Returns converted number.
 * @param {ArrayBuffer} data - Input buffer.
 * @param {number} offset - Start of data.
 * @return Converted number.
 * @public
 */
function littleEndianToUint32(data, offset)
{
	return (littleEndianToUint8(data, offset + 3) << 24) +
		(littleEndianToUint8(data, offset + 2) << 16) +
		(littleEndianToUint8(data, offset + 1) << 8) +
		littleEndianToUint8(data, offset)
}

/**
 * Interpret byte buffer as little endian 8 bit integer.
 * Returns converted number.
 * @param {ArrayBuffer} data - Input buffer.
 * @param {number} offset - Start of data.
 * @return Converted number.
 * @public
 */
function littleEndianToInt8(data, offset)
{
	var x = littleEndianToUint8(data, offset)
	if (x & 0x80) x = x - 256
	return x
}

/**
 * Interpret byte buffer as unsigned little endian 8 bit integer.
 * Returns converted number.
 * @param {ArrayBuffer} data - Input buffer.
 * @param {number} offset - Start of data.
 * @return Converted number.
 * @public
 */
function littleEndianToUint8(data, offset)
{
	return data[offset]
}

})(); // End of closure for parseAdvertisementData.


/**
 * Success callback function for getBondedDevices.
 * Called with array of bonded devices (may be empty).
 * @callback getBondedDevicesCallback
 * @param {Array} devices - Array of {DeviceInfo} objects. Note that
 * only fields name and address are available in the device info object.
 */

/**
 * Options for getBondedDevices.
 * @typedef {Object} GetBondedDevicesOptions
 * @param {array} serviceUUIDs - Array with or or more service UUID strings (mandatory).
 */

/**
 * Get a list of bonded devices.
 * @param {getBondedDevicesCallback} success - Callback function
 * called with list of bonded devices.
 * @param {failCallback} fail - Error callback function.
 * @param {GetBondedDevicesOptions} options - Mandatory object
 * that specifies service UUIDs to search for.
 * @example
 * evothings.ble.getBondedDevices(
 *     function(devices)
 *     {
 *         console.log('Bonded devices: ' + JSON.stringify(devices));
 *     },
 *     function(errorCode)
 *     {
 *         console.log('getBondedDevices error: ' + errorCode);
 *     },
 *     { serviceUUIDs: ['0000180a-0000-1000-8000-00805f9b34fb'] });
 */
exports.getBondedDevices = function(success, fail, options)
{
	exec(success, fail, 'BLE', 'getBondedDevices', [options.serviceUUIDs]);
}

/**
 * Success callback function for getBondState.
 * @callback getBondStateCallback
 * @param {string} state - The bond state of the device.
 * Possible values are: 'bonded', 'bonding' (Android only),
 * 'unbonded', and 'unknown'.
 */

/**
 * Options for getBondState.
 * @typedef {Object} GetBondStateOptions
 * @param {string} serviceUUID - String with service UUID (mandatory on iOS,
 * ignored on Android).
 */

/**
 * Get bond state for device.
 * @param {DeviceInfo} device - Object with address of the device
 * (a device object that contains just the address field may be used).
 * On iOS the address is a UUID, on Android the address is a MAC address.
 * This value can be found in the device objects obtained using startScan().
 * @param {getBondStateCallback} success - Callback function
 * called with the current bond state (a string).
 * @param {failCallback} fail - Error callback function.
 * @param {GetBondStateOptions} options - Mandatory on iOS where
 * a serviceUUID of the device must be specified. Ignored on Android.
 * @example
 * evothings.ble.getBondState(
 *     { address: uuidOrMacAddress }
 *     function(state)
 *     {
 *         console.log('Bond state: ' + state);
 *     },
 *     function(errorCode)
 *     {
 *         console.log('getBondState error: ' + errorCode);
 *     },
 *     { serviceUUID: '0000180a-0000-1000-8000-00805f9b34fb' });
 */
exports.getBondState = function(device, success, fail, options)
{
	// On iOS we must provide a service UUID.
	var serviceUUID = (options && options.serviceUUID) ? options.serviceUUID : null;

	if (exports.os.isAndroid())
	{
		// On Android we call the native getBondState function.
		// Note that serviceUUID is ignored on Android.
		exec(success, fail, 'BLE', 'getBondState', [device.address, serviceUUID]);
	}
	else
	{
		// On iOS (and other platforms in the future) we get the list of
		// bonded devices and search it.
		exports.getBondedDevices(
			// Success function.
			function(devices)
			{
				for (var i in devices)
				{
					var d = devices[i];
					if (d.address == device.address)
					{
						success("bonded");
						return; // bonded device found
					}
				}
				success("unbonded")
			},
			// Error function.
			function(error)
			{
				success("unknown");
			},
			{ serviceUUIDs: [serviceUUID] }
		);
	}
}

/**
 * Success callback function for bond. On iOS the bond state returned
 * will always be 'unknown' (this function is a NOP on iOS). Note that
 * bonding on Android may fail and then this function is called with
 * 'unbonded' as the new state.
 * @callback bondCallback
 * @param {string} newState - The new bond state of the device.
 * Possible values are: 'bonded' (Android), 'bonding' (Android),
 * 'unbonded' (Android), and 'unknown' (iOS).
 */

/**
 * Bond with device. This function shows a pairing UI on Android.
 * Does nothing on iOS (on iOS paring cannot be requested programatically).
 * @param {DeviceInfo} device - Object with address of the device
 * (a device object that contains just the address field may be used).
 * On iOS the address is a UUID, on Android the address is a MAC address.
 * This value can be found in the device objects obtained using startScan().
 * @param {bondCallback} success - Callback function
 * called with the new bond state (a string). On iOS the result is
 * always 'unknown'.
 * @param {failCallback} fail - Error callback function.
 * @example
 * evothings.ble.bond(
 *     { address: uuidOrMacAddress }
 *     function(newState)
 *     {
 *         console.log('New bond state: ' + newState);
 *     },
 *     function(errorCode)
 *     {
 *         console.log('bond error: ' + errorCode);
 *     });
 */
exports.bond = function(device, success, fail)
{
	exec(success, fail, 'BLE', 'bond', [device.address]);
}

/**
 * Success callback function for unbond. On iOS the bond state returned
 * will always be 'unknown' (this function is a NOP on iOS). On Anroid
 * the result should be 'unbonded', but other states are possible. Check
 * the state to make sure the function was successful.
 * @callback unbondCallback
 * @param {string} newState - The new bond state of the device.
 * Possible values are: 'unbonded' (Android), 'bonding' (Android),
 * 'bonded' (Android), and 'unknown' (iOS).
 */

/**
 * Unbond with device. This function does nothing on iOS.
 * @param {DeviceInfo} device - Object with address of the device
 * (a device object that contains just the address field may be used).
 * On iOS the address is a UUID, on Android the address is a MAC address.
 * This value can be found in the device objects obtained using startScan().
 * @param {unbondCallback} success - Callback function
 * called with the new bond state (a string). On iOS the result is
 * always 'unknown'.
 * @param {failCallback} fail - Error callback function.
 * @example
 * evothings.ble.unbond(
 *     { address: uuidOrMacAddress }
 *     function(newState)
 *     {
 *         console.log('New bond state: ' + newState);
 *     },
 *     function(errorCode)
 *     {
 *         console.log('bond error: ' + errorCode);
 *     });
 */
exports.unbond = function(device, success, fail)
{
	exec(success, fail, 'BLE', 'unbond', [device.address]);
}

/**
 * Connect to a remote device. It is recommended that you use the high-level
 * function {evothings.ble.connectToDevice} in place of this function.
 * On Android connect may fail with error 133. If this happens, wait about 500ms
 * and connect again.
 * @param {DeviceInfo} device - Device object from scanCallback (for backwards
 * compatibility, this parameter may also be the address string of the device object).
 * @param {connectCallback} success
 * @param {failCallback} fail
 * @example
 * evothings.ble.connect(
 *     device,
 *     function(connectInfo)
 *     {
 *         console.log('Connect status for device: '
 *             + connectInfo.device.name
 *             + ' state: '
 *             + connectInfo.state);
 *     },
 *     function(errorCode)
 *     {
 *         console.log('Connect error: ' + errorCode);
 *     });
 */
exports.connect = function(deviceOrAddress, success, fail)
{
	if (typeof deviceOrAddress == 'string')
	{
		var address = deviceOrAddress;
		exec(success, fail, 'BLE', 'connect', [address]);
	}
	else
	if (typeof deviceOrAddress == 'object')
	{
		var device = deviceOrAddress;
		function onSuccess(connectInfo)
		{
			connectInfo.device = device;
			device.handle = connectInfo.deviceHandle;
			success(connectInfo);
		}
		exec(onSuccess, fail, 'BLE', 'connect', [device.address]);
	}
	else
	{
		fail('Invalid first argument');
	}
};

/**
 * Will be called whenever the device's connection state changes.
 * @callback connectCallback
 * @param {ConnectInfo} info
 */

/**
 * Info about connection events and state.
 * @typedef {Object} ConnectInfo
 * @property {DeviceInfo} device - The device object is available in the
 * ConnectInfo if a device object was passed to connect; passing the address
 * string to connect is allowed for backwards compatibility, but this does not
 * set the device field.
 * @property {number} deviceHandle - Handle to the device.
 * @property {number} state - One of the {@link module:cordova-plugin-ble.connectionState} keys.
 */

/**
 * A map describing possible connection states.
 * @alias module:cordova-plugin-ble.connectionState
 * @readonly
 * @enum
 */
exports.connectionState = {
	/** STATE_DISCONNECTED */
	0: 'STATE_DISCONNECTED',
	/** STATE_CONNECTING */
	1: 'STATE_CONNECTING',
	/** STATE_CONNECTED */
	2: 'STATE_CONNECTED',
	/** STATE_DISCONNECTING */
	3: 'STATE_DISCONNECTING',

	/** 0 */
	'STATE_DISCONNECTED': 0,
	/** 1 */
	'STATE_CONNECTING': 1,
	/** 2 */
	'STATE_CONNECTED': 2,
	/** 3 */
	'STATE_DISCONNECTING': 3,
};

/**
 * Connect to a BLE device and discover services. This is a more high-level
 * function than {evothings.ble.connect}. You can configure which services
 * to discover and also turn off automatic service discovery by supplying
 * an options parameter.
 * On Android connect may fail with error 133. If this happens, wait about 500ms
 * and connect again.
 * @param {DeviceInfo} device - Device object from {scanCallback}.
 * @param {connectedCallback} connected - Called when connected to the device.
 * @param {disconnectedCallback} disconnected - Called when disconnected from the device.
 * @param {failCallback} fail - Called on error.
 * @param {ConnectOptions} options - Optional connect options object.
 * @example
 *   evothings.ble.connectToDevice(
 *     device,
 *     function(device)
 *     {
 *       console.log('Connected to device: ' + device.name);
 *     },
 *     function(device)
 *     {
 *       console.log('Disconnected from device: ' + device.name);
 *     },
 *     function(errorCode)
 *     {
 *       console.log('Connect error: ' + errorCode);
 *     });
 */
exports.connectToDevice = function(device, connected, disconnected, fail, options)
{
	// Default options.
	var discoverServices = true;
	var serviceUUIDs = null;

	// Set options.
	if (options && (typeof options == 'object'))
	{
		if (options.discoverServices === false)
		{
			discoverServices = false;
		}

		if (Array.isArray(options.serviceUUIDs))
		{
			serviceUUIDs = options.serviceUUIDs;
		}
	}

	function onConnectEvent(connectInfo)
	{
		if (connectInfo.state == evothings.ble.connectionState.STATE_CONNECTED)
		{
			device.handle = connectInfo.deviceHandle;
			if (discoverServices)
			{
				// Read services, characteristics and descriptors.
				// device.services is set by readServiceData to
				// the resulting services array.
				evothings.ble.readServiceData(
					device,
					function readServicesSuccess(services)
					{
						// Notify connected callback.
						connected(device);
					},
					fail,
					{ serviceUUIDs: serviceUUIDs });
			}
			else
			{
				// Call connected callback without auto discovery of services.
				connected(device);
			}
		}
		else if (connectInfo.state == evothings.ble.connectionState.STATE_DISCONNECTED)
		{
			// Call disconnected callback.
			disconnected(device);
		}

    }

    // Connect to device.
	exec(onConnectEvent, fail, 'BLE', 'connect', [device.address]);
};

/**
 * Options for connectToDevice.
 * @typedef {Object} ConnectOptions
 * @property {boolean} discoverServices - Set to false to disable
 * automatic service discovery. Default is true.
 * @property {array} serviceUUIDs - Array with service UUID strings for
 * services to discover (optional). If empty or null, all services are
 * read, this is the default.
 */

/**
 * Get the handle of an object. If a handle is passed return it.
 * Allows to pass in either an object or a handle to API functions.
 * @private
 */
function objectHandle(objectOrHandle)
{
	if ((typeof objectOrHandle == 'object') && objectOrHandle.handle)
	{
		// It's an object, return the handle.
		return objectOrHandle.handle;
	}
	else
	{
		// It's a handle.
		return objectOrHandle;
	}
}

/**
 * Close the connection to a remote device.
 * <p>Frees any native resources associated with the device.
 * <p>Does not cause any callbacks to the function passed to connect().
 *
 * @param {DeviceInfo} device - Device object or a device handle
 * from {@link connectCallback}.
 * @example
 *   evothings.ble.close(device);
 */
exports.close = function(deviceOrHandle)
{
	exec(null, null, 'BLE', 'close', [objectHandle(deviceOrHandle)]);
};

/**
 * Fetch the remote device's RSSI (signal strength).
 * @param {DeviceInfo} device - Device object or a device handle from {@link connectCallback}.
 * @param {rssiCallback} success
 * @param {failCallback} fail
 * @example
 *   evothings.ble.rssi(
 *     device,
 *     function(rssi)
 *     {
 *       console.log('rssi: ' + rssi);
 *     },
 *     function(errorCode)
 *     {
 *       console.log('rssi error: ' + errorCode);
 *     });
 */
exports.rssi = function(deviceOrHandle, success, fail)
{
	exec(deviceOrHandle, success, fail, 'BLE', 'rssi', [objectHandle(deviceOrHandle)]);
};

/**
 * This function is called with an RSSI value.
 * @callback rssiCallback
 * @param {number} rssi - A negative integer, the signal strength in decibels.
 */

/**
 * Fetch information about a remote device's services.
 * @param {DeviceInfo} device - Device object or a device handle from {@link connectCallback}.
 * @param {serviceCallback} success - Called with array of {@link Service} objects.
 * @param {failCallback} fail
 * @example
 *     evothings.ble.services(
 *     device,
 *     function(services)
 *     {
 *       console.log('found services:');
 *       for (var i = 0; i < services.length; i++)
 *       {
 *         var service = services[i];
 *         console.log('  service:');
 *         console.log('    ' + service.handle);
 *         console.log('    ' + service.uuid);
 *         console.log('    ' + service.serviceType);
 *       }
 *     },
 *     function(errorCode)
 *     {
 *       console.log('services error: ' + errorCode);
 *     });
 */
exports.services = function(deviceOrHandle, success, fail)
{
	exec(success, fail, 'BLE', 'services', [objectHandle(deviceOrHandle)]);
};

/**
 * @callback serviceCallback
 * @param {Array} services - Array of {@link Service} objects.
 */

/**
 * Describes a GATT service.
 * @typedef {Object} Service
 * @property {number} handle
 * @property {string} uuid - Formatted according to RFC 4122, all lowercase.
 * @property {module:cordova-plugin-ble.serviceType} type
 */

/**
 * A map describing possible service types.
 * @readonly
 * @alias module:cordova-plugin-ble.serviceType
 * @enum
 */
exports.serviceType = {
	/** SERVICE_TYPE_PRIMARY */
	0: 'SERVICE_TYPE_PRIMARY',
	/** SERVICE_TYPE_SECONDARY */
	1: 'SERVICE_TYPE_SECONDARY',

	/** 0 */
	'SERVICE_TYPE_PRIMARY': 0,
	/** 1 */
	'SERVICE_TYPE_SECONDARY': 1,
};

/** Fetch information about a service's characteristics.
 * @param {DeviceInfo} device - Device object or a device handle from {@link connectCallback}.
 * @param {Service} service - Service object or handle from {@link serviceCallback}.
 * @param {characteristicCallback} success - Called with array of {@link Characteristic} objects.
 * @param {failCallback} fail
 * @example
 *   evothings.ble.characteristics(
 *     device,
 *     service,
 *     function(characteristics)
 *     {
 *       console.log('found characteristics:');
 *       for (var i = 0; i < characteristics.length; i++)
 *       {
 *         var characteristic = characteristics[i];
 *         console.log('  characteristic: ' + characteristic.uuid);
 *       }
 *     },
 *     function(errorCode)
 *     {
 *       console.log('characteristics error: ' + errorCode);
 *     });
 */
exports.characteristics = function(deviceOrHandle, serviceOrHandle, success, fail)
{
	exec(success, fail, 'BLE', 'characteristics',
		[objectHandle(deviceOrHandle),
		 objectHandle(serviceOrHandle)]);
};

/**
 * @callback characteristicCallback
 * @param {Array} characteristics - Array of {@link Characteristic} objects.
 */

/**
 * Describes a GATT characteristic.
 * @typedef {Object} Characteristic
 * @property {number} handle
 * @property {string} uuid - Formatted according to RFC 4122, all lowercase.
 * @property {module:cordova-plugin-ble.permission} permissions - Bitmask of
 * zero or more permission flags.
 * @property {module:cordova-plugin-ble.property} properties - Bitmask of
 * zero or more property flags.
 * @property {module:cordova-plugin-ble.writeType} writeType
 */

/**
 * A map describing possible permission flags.
 * @alias module:cordova-plugin-ble.permission
 * @readonly
 * @enum
 */
exports.permission = {
	/** PERMISSION_READ */
	1: 'PERMISSION_READ',
	/** PERMISSION_READ_ENCRYPTED */
	2: 'PERMISSION_READ_ENCRYPTED',
	/** PERMISSION_READ_ENCRYPTED_MITM */
	4: 'PERMISSION_READ_ENCRYPTED_MITM',
	/** PERMISSION_WRITE */
	16: 'PERMISSION_WRITE',
	/** PERMISSION_WRITE_ENCRYPTED */
	32: 'PERMISSION_WRITE_ENCRYPTED',
	/** PERMISSION_WRITE_ENCRYPTED_MITM */
	64: 'PERMISSION_WRITE_ENCRYPTED_MITM',
	/** PERMISSION_WRITE_SIGNED */
	128: 'PERMISSION_WRITE_SIGNED',
	/** PERMISSION_WRITE_SIGNED_MITM */
	256: 'PERMISSION_WRITE_SIGNED_MITM',

	/** 1 */
	'PERMISSION_READ': 1,
	/** 2 */
	'PERMISSION_READ_ENCRYPTED': 2,
	/** 4 */
	'PERMISSION_READ_ENCRYPTED_MITM': 4,
	/** 16 */
	'PERMISSION_WRITE': 16,
	/** 32 */
	'PERMISSION_WRITE_ENCRYPTED': 32,
	/** 64 */
	'PERMISSION_WRITE_ENCRYPTED_MITM': 64,
	/** 128 */
	'PERMISSION_WRITE_SIGNED': 128,
	/** 256 */
	'PERMISSION_WRITE_SIGNED_MITM': 256,
};

/**
 * A map describing possible property flags.
 * @alias module:cordova-plugin-ble.property
 * @readonly
 * @enum
 */
exports.property = {
	/** PROPERTY_BROADCAST */
	1: 'PROPERTY_BROADCAST',
	/** PROPERTY_READ */
	2: 'PROPERTY_READ',
	/** PROPERTY_WRITE_NO_RESPONSE */
	4: 'PROPERTY_WRITE_NO_RESPONSE',
	/** PROPERTY_WRITE */
	8: 'PROPERTY_WRITE',
	/** PROPERTY_NOTIFY */
	16: 'PROPERTY_NOTIFY',
	/** PROPERTY_INDICATE */
	32: 'PROPERTY_INDICATE',
	/** PROPERTY_SIGNED_WRITE */
	64: 'PROPERTY_SIGNED_WRITE',
	/** PROPERTY_EXTENDED_PROPS */
	128: 'PROPERTY_EXTENDED_PROPS',

	/** 1 */
	'PROPERTY_BROADCAST': 1,
	/** 2 */
	'PROPERTY_READ': 2,
	/** 4 */
	'PROPERTY_WRITE_NO_RESPONSE': 4,
	/** 8 */
	'PROPERTY_WRITE': 8,
	/** 16 */
	'PROPERTY_NOTIFY': 16,
	/** 32 */
	'PROPERTY_INDICATE': 32,
	/** 64 */
	'PROPERTY_SIGNED_WRITE': 4,
	/** 128 */
	'PROPERTY_EXTENDED_PROPS': 128,
};

/**
 * A map describing possible write types.
 * @alias module:cordova-plugin-ble.writeType
 * @readonly
 * @enum
 */
exports.writeType = {
	/** WRITE_TYPE_NO_RESPONSE */
	1: 'WRITE_TYPE_NO_RESPONSE',
	/** WRITE_TYPE_DEFAULT */
	2: 'WRITE_TYPE_DEFAULT',
	/** WRITE_TYPE_SIGNED */
	4: 'WRITE_TYPE_SIGNED',

	/** 1 */
	'WRITE_TYPE_NO_RESPONSE': 1,
	/** 2 */
	'WRITE_TYPE_DEFAULT': 2,
	/** 4 */
	'WRITE_TYPE_SIGNED': 4,
};

/**
 * Fetch information about a characteristic's descriptors.
 * @param {DeviceInfo} device - Device object or a device handle from
 * {@link connectCallback}.
 * @param {Characteristic} characteristic - Characteristic object or handle
 * from {@link characteristicCallback}.
 * @param {descriptorCallback} success - Called with array of {@link Descriptor} objects.
 * @param {failCallback} fail
 * @example
 *   evothings.ble.descriptors(
 *     device,
 *     characteristic,
 *     function(descriptors)
 *     {
 *       console.log('found descriptors:');
 *       for (var i = 0; i < descriptors.length; i++)
 *       {
 *         var descriptor = descriptors[i];
 *         console.log('  descriptor: ' + descriptor.uuid);
 *       }
 *     },
 *     function(errorCode)
 *     {
 *       console.log('descriptors error: ' + errorCode);
 *     });
 */
exports.descriptors = function(deviceOrHandle, characteristicOrHandle, success, fail)
{
	exec(success, fail, 'BLE', 'descriptors',
		[objectHandle(deviceOrHandle),
		 objectHandle(characteristicOrHandle)]);
};

/**
 * @callback descriptorCallback
 * @param {Array} descriptors - Array of {@link Descriptor} objects.
 */

/**
 * Describes a GATT descriptor.
 * @typedef {Object} Descriptor
 * @property {number} handle
 * @property {string} uuid - Formatted according to RFC 4122, all lowercase.
 * @property {module:cordova-plugin-ble.permission} permissions - Bitmask of
 * zero or more permission flags.
 */

/**
 * @callback dataCallback
 * @param {ArrayBuffer} data
 */

/**
 * Reads a characteristic's value from a remote device.
 * @param {DeviceInfo} device - Device object or a device handle from
 * {@link connectCallback}.
 * @param {Characteristic} characteristic - Characteristic object or handle
 * from {@link characteristicCallback}.
 * @param {dataCallback} success
 * @param {failCallback} fail
 * @example
 *   evothings.ble.readCharacteristic(
 *     device,
 *     characteristic,
 *     function(data)
 *     {
 *       console.log('characteristic data: ' + evothings.ble.fromUtf8(data));
 *     },
 *     function(errorCode)
 *     {
 *       console.log('readCharacteristic error: ' + errorCode);
 *     });
 */
exports.readCharacteristic = function(deviceOrHandle, characteristicOrHandle, success, fail)
{
	exec(success, fail, 'BLE', 'readCharacteristic',
		[objectHandle(deviceOrHandle),
		 objectHandle(characteristicOrHandle)]);
};

/**
 * Reads a descriptor's value from a remote device.
 * @param {DeviceInfo} device - Device object or a device handle from {@link connectCallback}.
 * @param {Descriptor} descriptor - Descriptor object or handle from {@link descriptorCallback}.
 * @param {dataCallback} success
 * @param {failCallback} fail
 * @example
 * evothings.ble.readDescriptor(
 *   device,
 *   descriptor,
 *   function(data)
 *   {
 *     console.log('descriptor data: ' + evothings.ble.fromUtf8(data));
 *   },
 *   function(errorCode)
 *   {
 *     console.log('readDescriptor error: ' + errorCode);
 *   });
 */
exports.readDescriptor = function(deviceOrHandle, descriptorOrHandle, success, fail)
{
	exec(success, fail, 'BLE', 'readDescriptor',
		[objectHandle(deviceOrHandle),
		 objectHandle(descriptorOrHandle)]);
};

/**
 * @callback emptyCallback - Callback that takes no parameters.
 * This callback indicates that an operation was successful,
 * without specifying and additional information.
 */

/**
 * Write a characteristic's value to the remote device.
 *
 * Writes with response, the remote device sends back a confirmation message.
 * This is safe but slower than writing without response.
 *
 * @param {DeviceInfo} device - Device object or a device handle from
 * {@link connectCallback}.
 * @param {Characteristic} characteristic - Characteristic object or handle
 * from {@link characteristicCallback}.
 * @param {ArrayBufferView} data - The value to be written.
 * @param {emptyCallback} success - Called when the remote device has
 * confirmed the write.
 * @param {failCallback} fail - Called if the operation fails.
 * @example TODO: Add example.
 */
exports.writeCharacteristic = function(deviceOrHandle, characteristicOrHandle, data, success, fail)
{
	exec(success, fail, 'BLE', 'writeCharacteristic',
		[objectHandle(deviceOrHandle),
		 objectHandle(characteristicOrHandle),
		 data.buffer]);
};

/**
 * Write a characteristic's value without response.
 *
 * Asks the remote device to NOT send a confirmation message.
 * This may be used for increased data throughput.
 *
 * If the application needs to ensure data integrity, a separate safety protocol
 * would be required. Design of such protocols is beyond the scope of this document.
 *
 * @param {DeviceInfo} device - Device object or a device handle from
 * {@link connectCallback}.
 * @param {Characteristic} characteristic - Characteristic object or handle
 * from {@link characteristicCallback}.
 * @param {ArrayBufferView} data - The value to be written.
 * @param {emptyCallback} success - Called when the data has been sent.
 * @param {failCallback} fail - Called if the operation fails.
 */
exports.writeCharacteristicWithoutResponse = function(deviceOrHandle, characteristicOrHandle, data, success, fail)
{
	exec(success, fail, 'BLE', 'writeCharacteristicWithoutResponse',
		[objectHandle(deviceOrHandle),
		 objectHandle(characteristicOrHandle),
		 data.buffer]);
};

/**
 * Write a descriptor's value to a remote device.
 * @param {DeviceInfo} device - Device object or a device handle from {@link connectCallback}.
 * @param {Descriptor} descriptor - Descriptor object or handle from {@link descriptorCallback}.
 * @param {ArrayBufferView} data - The value to be written.
 * @param {emptyCallback} success
 * @param {failCallback} fail
 * @example TODO: Add example.
 */
exports.writeDescriptor = function(deviceOrHandle, descriptorOrHandle, data, success, fail)
{
	exec(success, fail, 'BLE', 'writeDescriptor',
		[objectHandle(deviceOrHandle),
		 objectHandle(descriptorOrHandle),
		 data.buffer]);
};

/**
 * Request notification or indication on changes to a characteristic's value.
 * This is more efficient than polling the value using readCharacteristic().
 * This function automatically detects if the characteristic supports
 * notification or indication.
 *
 * <p>Android only: To disable this functionality and write
 * the configuration descriptor yourself, supply an options object as
 * last parameter, see example below.</p>
 *
 * @param {DeviceInfo} device - Device object or a device handle from
 * {@link connectCallback}.
 * @param {Characteristic} characteristic - Characteristic object or handle
 * from {@link characteristicCallback}.
 * @param {dataCallback} success - Called every time the value changes.
 * @param {failCallback} fail - Error callback.
 * @param {NotificationOptions} options - Android only: Optional object with options.
 * Set field writeConfigDescriptor to false to disable automatic writing of
 * notification or indication descriptor value. This is useful if full control
 * of writing the config descriptor is needed.
 *
 * @example
 *   // Example call:
 *   evothings.ble.enableNotification(
 *     device,
 *     characteristic,
 *     function(data)
 *     {
 *       console.log('characteristic data: ' + evothings.ble.fromUtf8(data));
 *     },
 *     function(errorCode)
 *     {
 *       console.log('enableNotification error: ' + errorCode);
 *     });
 *
 *   // To disable automatic writing of the config descriptor
 *   // supply this as last parameter to enableNotification:
 *   { writeConfigDescriptor: false }
 */
exports.enableNotification = function(deviceOrHandle, characteristicOrHandle, success, fail, options)
{
	var flags = 0;
	if (options && (false === options.writeConfigDescriptor))
	{
		var flags = 1; // Don't write config descriptor.
	}
	exec(success, fail, 'BLE', 'enableNotification',
		[objectHandle(deviceOrHandle),
		 objectHandle(characteristicOrHandle),
		 flags]);
};

/**
 * Disable notification or indication of a characteristic's value.
 *
 * @param {DeviceInfo} device - Device object or a device handle from
 * {@link connectCallback}.
 * @param {Characteristic} characteristic - Characteristic object or handle
 * from {@link characteristicCallback}.
 * @param {emptyCallback} success - Success callback.
 * @param {failCallback} fail - Error callback.
 * @param {NotificationOptions} options - Android only: Optional object with options.
 * Set field writeConfigDescriptor to false to disable automatic writing of
 * notification or indication descriptor value. This is useful if full control
 * of writing the config descriptor is needed.
 *
 * @example
 *   // Example call:
 *   evothings.ble.disableNotification(
 *     device,
 *     characteristic,
 *     function()
 *     {
 *       console.log('characteristic notification disabled');
 *     },
 *     function(errorCode)
 *     {
 *       console.log('disableNotification error: ' + errorCode);
 *     });
 *
 *   // To disable automatic writing of the config descriptor
 *   // supply this as last parameter to enableNotification:
 *   { writeConfigDescriptor: false }
 */
exports.disableNotification = function(deviceOrHandle, characteristicOrHandle, success, fail, options) {
	var flags = 0;
	if (options && (false === options.writeConfigDescriptor))
	{
		var flags = 1; // Don't write config descriptor.
	}
	exec(success, fail, 'BLE', 'disableNotification',
		[objectHandle(deviceOrHandle),
		 objectHandle(characteristicOrHandle),
		 flags]);
};

/**
 * Options for enableNotification and disableNotification.
 * @typedef {Object} NotificationOptions
 * @property {boolean} writeConfigDescriptor - set to false to disable
 * automatic writing of the notification or indication descriptor.
 * This is useful if full control of writing the config descriptor is needed.
 */

/**
 * i is an integer. It is converted to byte and put in an array[1].
 * The array is returned.
 * <p>assert(string.charCodeAt(0) == i).
 *
 * @param {number} i
 * @param {dataCallback} success - Called every time the value changes.
 */
exports.testCharConversion = function(i, success)
{
	exec(success, null, 'BLE', 'testCharConversion', [i]);
};

/**
 * Resets the device's Bluetooth system.
 * This is useful on some buggy devices where BLE functions stops responding until reset.
 * Available on Android 4.3+. This function takes 3-5 seconds to reset BLE.
 * On iOS this function stops any ongoing scan operation and disconnects
 * all connected devices.
 *
 * @param {emptyCallback} success
 * @param {failCallback} fail
 */
exports.reset = function(success, fail)
{
	exec(success, fail, 'BLE', 'reset', []);
};

/**
 * Converts an ArrayBuffer containing UTF-8 data to a JavaScript String.
 * @param {ArrayBuffer} a
 * @returns string
 */
exports.fromUtf8 = function(a)
{
	return decodeURIComponent(escape(String.fromCharCode.apply(null, new Uint8Array(a))));
};

/**
 * Converts a JavaScript String to an Uint8Array containing UTF-8 data.
 * @param {string} s
 * @returns Uint8Array
 */
exports.toUtf8 = function(s)
{
	var strUtf8 = unescape(encodeURIComponent(s));
	var ab = new Uint8Array(strUtf8.length);
	for (var i = 0; i < strUtf8.length; i++)
	{
		ab[i] = strUtf8.charCodeAt(i);
	}
	return ab;
};

/**
 * Returns a canonical UUID.
 *
 * Code adopted from the Bleat library by Rob Moran (@thegecko), see this file:
 * https://github.com/thegecko/bleat/blob/master/dist/bluetooth.helpers.js
 *
 * @param {string|number} uuid - The UUID to turn into canonical form.
 * @return Canonical UUID.
 */
exports.getCanonicalUUID = function(uuid)
{
	if (typeof uuid === 'number')
	{
		uuid = uuid.toString(16);
	}

	uuid = uuid.toLowerCase();

	if (uuid.length <= 8)
	{
		uuid = ('00000000' + uuid).slice(-8) + '-0000-1000-8000-00805f9b34fb';
	}

	if (uuid.length === 32)
	{
		uuid = uuid
			.match(/^([0-9a-f]{8})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{12})$/)
			.splice(1)
			.join('-');
	}

	return uuid;
};

/**
 * Read all services, and associated characteristics and descriptors
 * for the given device.
 *
 * This function is an easy-to-use wrapper of the low-level functions
 * ble.services(), ble.characteristics() and ble.descriptors().
 *
 * @param {DeviceInfo} device - Device object or device handle
 * from {@link connectCallback}.
 * @param {serviceCallback} success - Called with array of {@link Service} objects.
 * Those Service objects each have an additional field "characteristics",
 * which is an array of {@link Characteristic} objects.
 * Those Characteristic objects each have an additional field "descriptors",
 * which is an array of {@link Descriptor} objects.
 * @param {failCallback} fail - Error callback.
 */
exports.readAllServiceData = function(deviceOrHandle, success, fail)
{
	exports.readServiceData(deviceOrHandle, success, fail);
}

/**
 * Options for readServiceData.
 * @typedef {Object} ReadServiceDataOptions
 * @property {array} serviceUUIDs - Array with service UUID strings for
 * services to discover (optional). If absent or null, all services are
 * read, this is the default.
 */

/**
 * Read services, and associated characteristics and descriptors
 * for the given device. Which services to read may be specified
 * in the options parameter. Leaving out the options parameter
 * with read all services.
 *
 * @param {DeviceInfo} device - Device object or device handle
 * from {@link connectCallback}.
 * @param {serviceCallback} success - Called with array of {@link Service} objects.
 * Those Service objects each have an additional field "characteristics",
 * which is an array of {@link Characteristic} objects.
 * Those Characteristic objects each have an additional field "descriptors",
 * which is an array of {@link Descriptor} objects.
 * @param {failCallback} fail - Error callback.
 * @param {ReadServiceDataOptions} options - Object with options
 * (optional parameter). If left out, all services are read.
 */
exports.readServiceData = function(deviceOrHandle, success, fail, options)
{
	// Set options.
	var serviceUUIDs = null;
	if (options && Array.isArray(options.serviceUUIDs))
	{
		serviceUUIDs = getCanonicalUUIDArray(options.serviceUUIDs);
	}

	// Array of populated services.
	var serviceArray = [];

	// Counter that tracks the number of info items read.
	// This value is incremented and decremented when reading.
	// When value is back to zero, all items are read.
	var readCounter = 0;

	function includeService(service)
	{
		if (serviceUUIDs)
		{
			// Include service only if in array.
			return serviceUUIDs.indexOf(service.uuid) > -1;
		}
		else
		{
			// Include all services.
			return true;
		}
	}

	function servicesCallbackFun()
	{
		return function(services)
		{
			readCounter += services.length;
			for (var i = 0; i < services.length; ++i)
			{
				var service = services[i];
				service.uuid = exports.getCanonicalUUID(service.uuid);
				if (includeService(service))
				{
					// Save service.
					serviceArray.push(service);
					service.characteristics = [];

					// Read characteristics for service.
					exports.characteristics(
						deviceOrHandle,
						service,
						characteristicsCallbackFun(service),
						function(errorCode)
						{
							fail(errorCode);
						});
				}
				else
				{
					// Service not included, but reduce readCounter.
					--readCounter;
				}
			}
		};
	}

	function characteristicsCallbackFun(service)
	{
		return function(characteristics)
		{
			--readCounter;
			readCounter += characteristics.length;
			for (var i = 0; i < characteristics.length; ++i)
			{
				var characteristic = characteristics[i];
				characteristic.uuid = exports.getCanonicalUUID(characteristic.uuid);
				service.characteristics.push(characteristic);
				characteristic.descriptors = [];

				// Read descriptors for characteristic.
				exports.descriptors(
					deviceOrHandle,
					characteristic,
					descriptorsCallbackFun(characteristic),
					function(errorCode)
					{
						console.log('descriptors error: ' + errorCode);
						fail(errorCode);
					});
			}
		};
	}

	function descriptorsCallbackFun(characteristic)
	{
		return function(descriptors)
		{
			--readCounter;
			for (var i = 0; i < descriptors.length; ++i)
			{
				var descriptor = descriptors[i];
				descriptor.uuid = exports.getCanonicalUUID(descriptor.uuid);
				characteristic.descriptors.push(descriptor);
			}
			if (0 == readCounter)
			{
				// Everything is read. If a device object is supplied,
				// set the services array of the device to the result.
				if (typeof deviceOrHandle == 'object')
				{
					deviceOrHandle.services = serviceArray;
				}

				// Call result function.
				success(serviceArray);
			}
		};
	}

	// Read services for device.
	exports.services(
		deviceOrHandle,
		servicesCallbackFun(),
		function(errorCode)
		{
			console.log('services error: ' + errorCode);
			fail(errorCode);
		});
};

/**
 * Get a service object from a device or array.
 * @param {DeviceInfo} device - Device object (or array of {@link Service} objects).
 * @param {string} uuid - UUID of service to get.
 */
exports.getService = function(deviceOrServices, uuid)
{
	var services = null;

	if (Array.isArray(deviceOrServices))
	{
		// First arg is a service array.
		services = deviceOrServices;
	}
	else if (deviceOrServices && Array.isArray(deviceOrServices.services))
	{
		// First arg is a device object.
		services = deviceOrServices.services;
	}
	else
	{
		// First arg is invalid.
		return null;
	}

	// Normalize UUID.
	uuid = exports.getCanonicalUUID(uuid);

	for (var i in services)
	{
		var service = services[i];
		if (service.uuid == uuid)
		{
			return service;
		}
	}

	return null;
};

/**
 * Get a characteristic object of a service. (Characteristics
 * within a service that share the same UUID (rare case) must
 * be retrieved by manually traversing the characteristics
 * array of the service. This function will return the first
 * characteristic found, which may not be the one you want.
 * Note that this is a rare case.)
 * @param {Service} device - Service object.
 * @param {string} uuid - UUID of characteristic to get.
 */
exports.getCharacteristic = function(service, uuid)
{
	uuid = exports.getCanonicalUUID(uuid);

	var characteristics = service.characteristics;
	for (var i in characteristics)
	{
		var characteristic = characteristics[i];
		if (characteristic.uuid == uuid)
		{
			return characteristic;
		}
	}

	return null;
};

/**
 * Get a descriptor object of a characteristic.
 * @param {Characteristic} characteristic - Characteristic object.
 * @param {string} uuid - UUID of descriptor to get.
 */
exports.getDescriptor = function(characteristic, uuid)
{
	uuid = exports.getCanonicalUUID(uuid);

	var descriptors = characteristic.descriptors;
	for (var i in descriptors)
	{
		var descriptor = descriptors[i];
		if (descriptor.uuid == uuid)
		{
			return descriptor;
		}
	}

	return null;
};


/********** Platform utilities **********/

exports.os = (window.evothings && window.evothings.os) ? window.evothings.os : {}

/**
 * Returns true if current platform is iOS, false if not.
 * @return {boolean} true if platform is iOS, false if not.
 * @public
 */
exports.os.isIOS = function()
{
	return /iP(hone|ad|od)/.test(navigator.userAgent);
};

/**
 * Returns true if current platform is Android, false if not.
 * @return {boolean} true if platform is Android, false if not.
 * @public
 */
exports.os.isAndroid = function()
{
	return /Android|android/.test(navigator.userAgent);
};

/********** BLE Peripheral API **********/

/**
 * BLE Peripheral API. Experimental, supported only on Android.
 * @namespace
 */
exports.peripheral = {}

// Internal. Returns a function that will handle GATT server callbacks.
function gattServerCallbackHandler(winFunc, settings) {
	// collect read/write callbacks and add handles, so the native side can tell us which one to call.
	var readCallbacks = {};
	var writeCallbacks = {};
	var nextHandle = 1;

	function handleCallback(object, name, callbacks) {
		if(!object[name]) {
			throw name+" missing!";
		}
		callbacks[nextHandle] = object[name];
		object[name+"Handle"] = nextHandle;
		nextHandle += 1;
	}

	function handleReadWrite(object) {
		/* // primitive version
		if(!object.readRequestCallback) {
			throw "readRequestCallback missing!");
		}
		readCallbacks[nextHandle] = object.readRequestCallback;
		*/
		handleCallback(object, "onReadRequest", readCallbacks);
		handleCallback(object, "onWriteRequest", writeCallbacks);
	}

	for(var i=0; i<settings.services.length; i++) {
		var service = settings.services[i];
		for(var j=0; j<service.characteristics.length; j++) {
			var characteristic = service.characteristics[j];
			handleReadWrite(characteristic);
			for(var k=0; k<characteristic.descriptors.length; k++) {
				var descriptor = characteristic.descriptors[k];
				handleReadWrite(descriptor);
			}
		}
	}

	settings.nextHandle = nextHandle;

	return function(args) {
		// primitive version
		/*if(args.name == "win") {
			winFunc();
			return;
		}*/
		var funcs = {
			win: winFunc,
			connection: function() {
				settings.onConnectionStateChange(args.deviceHandle, args.connected);
			},
			write: function() {
				writeCallbacks[args.callbackHandle](args.deviceHandle, args.requestId, args.data);
			},
			read: function() {
				readCallbacks[args.callbackHandle](args.deviceHandle, args.requestId);
			},
		};
		funcs[args.name]();
	};
}

/** Starts the GATT server.
* There can be only one server. If this function is called while the server is still running, the call will fail.
* Once this function succeeds, the server may be stopped by calling stopGattServer.
*
* @param {GattSettings} settings
* @param {emptyCallback} win
* @param {failCallback} fail
*/
exports.peripheral.startGattServer = function(settings, win, fail) {
	exec(gattServerCallbackHandler(win, settings), fail, 'BLE', 'startGattServer', [settings]);
};

// GattSettings
/** Describes a GATT server.
* @typedef {Object} GattSettings
* @property {Array} services - An array of GattService objects.
* @property {connectionStateChangeCallback} onConnectionStateChange
*/

/** Describes a GATT service.
* @typedef {Object} GattService
* @property {string} uuid - Formatted according to RFC 4122, all lowercase.
* @property {serviceType} type
* @property {Array} characteristics - An array of GattCharacteristic objects.
*/

/** Describes a GATT characteristic.
* @typedef {Object} GattCharacteristic
* @property {int} handle - Optional. Used in notify(). If set, must be unique among all other GattCharacteristic handles.
* @property {string} uuid - Formatted according to RFC 4122, all lowercase.
* @property {module:cordova-plugin-ble.permission} permissions - Bitmask of zero or more permission flags.
* @property {property} properties - Bitmask of zero or more property flags.
* @property {writeType} writeType
* @property {readRequestCallback} onReadRequest
* @property {writeRequestCallback} onWriteRequest
* @property {Array} descriptors - Optional. An array of GattDescriptor objects.
*/

/** Describes a GATT descriptor.
* @typedef {Object} GattDescriptor
* @property {string} uuid - Formatted according to RFC 4122, all lowercase.
* @property {module:cordova-plugin-ble.permission} permissions - Bitmask of zero or more permission flags.
* @property {readRequestCallback} onReadRequest
* @property {writeRequestCallback} onWriteRequest
*/


// GattServer callbacks
/** This function is a part of GattSettings and is called when a remote device connects to, or disconnects from, your server.
* @callback connectionStateChangeCallback
* @param {int} deviceHandle - Will be used in other callbacks.
* @param {boolean} connected - If true, the device just connected, and the handle is now valid for use in close() and other functions.
* If false, it just disconnected, and the handle is now invalid for use in close() and other functions.
*/

/** Called when a remote device asks to read a characteristic or descriptor.
* You must call sendResponse() to complete the request.
* @callback readRequestCallback
* @param {int} deviceHandle
* @param {int} requestId
*/

/** Called when a remote device asks to write a characteristic or descriptor.
* You must call sendResponse() to complete the request.
* @callback writeRequestCallback
* @param {int} deviceHandle
* @param {int} requestId
* @param {ArrayBuffer} data
*/


/** Stops the GATT server.
* This stops any active advertisements and forcibly disconnects any clients.
* There can be only one server. If startGattServer() returned success, you may call this function once.
* Calling it more will result in failure.
*
* @param {emptyCallback} win
* @param {failCallback} fail
*/
exports.peripheral.stopGattServer = function(win, fail) {
	exec(win, fail, 'BLE', 'stopGattServer', []);
};

/** Sends a response to a read or write request.
* @param {int} deviceHandle - From a requestCallback.
* @param {int} requestId - From the same requestCallback as deviceHandle.
* @param {ArrayBufferView} data - Required for responses to read requests. May be set to null for write requests.
* @param {emptyCallback} win
* @param {failCallback} fail
*/
exports.peripheral.sendResponse = function(deviceHandle, requestId, data, win, fail) {
	exec(win, fail, 'BLE', 'sendResponse', [deviceHandle, requestId, data.buffer]);
}

/** Sends a notification to a remote device that a characteristic's value has been updated.
* @param {int} deviceHandle - From a connectionStateChangeCallback.
* @param {int} characteristicHandle - GattCharacteristic.handle
* @param {ArrayBufferView} data - The characteristic's new value.
* @param {emptyCallback} win
* @param {failCallback} fail
*/
exports.peripheral.notify = function(deviceHandle, characteristic, data, win, fail) {
	exec(win, fail, 'BLE', 'notify', [deviceHandle, characteristic, data.buffer]);
};

/*	// never mind, just use close().
// Closes a client handle, freeing the resources.
exports.closeClient = function(clientHandle, win, fail) {
};
*/


/** Starts BLE advertise.
* Fails if advertise is running. In that case, call stopAdvertise first.
*
* @param {AdvertiseSettings} settings
* @param {emptyCallback} win
* @param {failCallback} fail
*/
exports.peripheral.startAdvertise = function(settings, win, fail) {
	exec(win, fail, 'BLE', 'startAdvertise', [settings]);
}

/** Stops BLE advertise.
*
* @param {emptyCallback} win
* @param {failCallback} fail
*/
exports.peripheral.stopAdvertise = function(win, fail) {
	exec(win, fail, 'BLE', 'stopAdvertise', []);
}

// AdvertiseSettings
/** Describes a BLE advertisement.
*
* All the properties are optional, except broadcastData.
*
* @typedef {Object} AdvertiseSettings
* @property {string} advertiseMode - ADVERTISE_MODE_LOW_POWER, ADVERTISE_MODE_BALANCED, or ADVERTISE_MODE_LOW_LATENCY.
* The default is ADVERTISE_MODE_LOW_POWER.
* @property {boolean} connectable - Advertise as connectable or not. Has no bearing on whether the device is actually connectable.
* The default is true if there is a GattServer running, false if there isn't.
* @property {int} timeoutMillis - Advertising time limit. May not exceed 180000 milliseconds. A value of 0 will disable the time limit.
* The default is 0.
* @property {string} txPowerLevel - ADVERTISE_TX_POWER_ULTRA_LOW, ADVERTISE_TX_POWER_LOW, ADVERTISE_TX_POWER_MEDIUM or ADVERTISE_TX_POWER_HIGH.
* The default is ADVERTISE_TX_POWER_MEDIUM.
* @property {AdvertiseData} broadcastData - The data which will be broadcast. Passive scanners will see this data.
* @property {AdvertiseData} scanResponseData - The data with which the device will respond to active scans.
* Should be an extension to the broadcastData; should not contain the same data.
*/

/** Describes BLE advertisement data.
*
* Data size is limited to 31 bytes. Each property set consumes some bytes.
* If too much data is added, startAdvertise will fail with "ADVERTISE_FAILED_DATA_TOO_LARGE" or something similar.
*
* All properties are optional.
* UUIDs must be formatted according to RFC 4122, all lowercase.
* Normally, UUIDs take up 16 bytes. However, UUIDs that use the Bluetooth Base format can be compressed to 4 or 2 bytes.
* The Bluetooth Base UUID is "00000000-0000-1000-8000-00805f9b34fb".
* For 2 bytes, use this format, where "x" is any hexadecimal digit: "0000xxxx-0000-1000-8000-00805f9b34fb".
* For 4 bytes, use this format: "xxxxxxxx-0000-1000-8000-00805f9b34fb".
*
* @typedef {Object} AdvertiseData
* @property {boolean} includeDeviceName - If true, the device's Bluetooth name is added to the advertisement.
* The name is set by the user in the device's Settings. The name cannot be changed by the app.
* The default is false.
* @property {boolean} includeTxPowerLevel - If true, the txPowerLevel found in AdvertiseSettings is added to the advertisement.
* The default is false.
* @property {Array} serviceUUIDs - Array of strings. Each string is the UUID of a service that should be available in the device's GattServer.
* @property {Object} serviceData - Map of string to string. Each key is a service UUID.
* The value is base64-encoded data associated with the service.
* @property {Object} manufacturerData - Map of int to string. Each key is a manufacturer ID.
* Manufacturer IDs are assigned by the {@link http://www.bluetooth.com/|Bluetooth Special Interest Group}.
* The value is base64-encoded data associated with the manufacturer.
*/