/* mbed Microcontroller Library
 * Copyright (c) 2017-2017 ARM Limited
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <stdio.h>
#include <stdlib.h>

#include <ble/DiscoveredService.h>
#include <ble/DiscoveredCharacteristic.h>
#include "ble/generic/GenericGattClient.h"
#include "ble/blecommon.h"
#include <algorithm>

using ble::pal::AttServerMessage;
using ble::pal::AttReadResponse;
using ble::pal::AttReadBlobResponse;
using ble::pal::AttReadByTypeResponse;
using ble::pal::AttReadByGroupTypeResponse;
using ble::pal::AttFindByTypeValueResponse;
using ble::pal::AttErrorResponse;
using ble::pal::AttributeOpcode;
using ble::pal::AttWriteResponse;
using ble::pal::AttPrepareWriteResponse;
using ble::pal::AttExecuteWriteResponse;
using ble::pal::AttHandleValueIndication;
using ble::pal::AttHandleValueNotification;
using ble::pal::AttFindInformationResponse;

namespace ble {
namespace generic {

/*
 * Type of procedures which can be launched by the client.
 */
enum procedure_type_t {
	COMPLETE_DISCOVERY_PROCEDURE,
	READ_PROCEDURE,
	WRITE_PROCEDURE,
	DESCRIPTOR_DISCOVERY_PROCEDURE
};


/*
 * Base class for a procedure control block
 */
struct procedure_control_block_t {
	/*
	 * Base constructor for procedure control block.
	 */
	procedure_control_block_t(procedure_type_t type, Gap::Handle_t handle) :
		type(type), connection_handle(handle), next(NULL) { }

	virtual ~procedure_control_block_t() { }

	/*
	 * Entry point of the control block stack machine.
	 */
	virtual void handle(GenericGattClient* client, const AttServerMessage& message) = 0;

	/*
	 * Function call in case of timeout
	 */
	virtual void handle_timeout_error(GenericGattClient* client) = 0;

	procedure_type_t type;
	Gap::Handle_t connection_handle;
	procedure_control_block_t* next;
};


/*
 * Procedure control block for the discovery process.
 */
struct discovery_control_block_t : public procedure_control_block_t {
	discovery_control_block_t(
		Gap::Handle_t handle,
		ServiceDiscovery::ServiceCallback_t service_callback,
		ServiceDiscovery::CharacteristicCallback_t characteristic_callback,
		UUID matching_service_uuid,
		UUID matching_characteristic_uuid
	) : procedure_control_block_t(COMPLETE_DISCOVERY_PROCEDURE, handle),
		service_callback(service_callback),
		characteristic_callback(characteristic_callback),
		matching_service_uuid(matching_service_uuid),
		matching_characteristic_uuid(matching_characteristic_uuid),
		services_discovered(NULL),
		done(false) {
	}

	virtual ~discovery_control_block_t() {
		while(services_discovered) {
			service_t* tmp = services_discovered->next;
			delete services_discovered;
			services_discovered = tmp;
		}
	}

	virtual void handle_timeout_error(GenericGattClient* client) {
		terminate(client);
	}

	virtual void handle(GenericGattClient* client, const AttServerMessage& message) {
		// if end of discovery has been requested, ends it immediately
		if (done) {
			terminate(client);
			return;
		}

		switch(message.opcode) {
			case AttributeOpcode::READ_BY_GROUP_TYPE_RESPONSE:
				handle_service_discovered(
					client, static_cast<const AttReadByGroupTypeResponse&>(message)
				);
			   break;
			case AttributeOpcode::FIND_BY_VALUE_TYPE_RESPONSE:
				handle_service_discovered(
					client, static_cast<const AttFindByTypeValueResponse&>(message)
				);
			    break;
			case AttributeOpcode::READ_BY_TYPE_RESPONSE:
				handle_characteristic_discovered(
					client, static_cast<const AttReadByTypeResponse&>(message)
				);
			    break;
			case AttributeOpcode::ERROR_RESPONSE: {
				const AttErrorResponse& error = static_cast<const AttErrorResponse&>(message);
				if (error.error_code != AttErrorResponse::ATTRIBUTE_NOT_FOUND) {
					terminate(client);
				}

				switch (error.request_opcode) {
					case AttributeOpcode::READ_BY_GROUP_TYPE_REQUEST:
					case AttributeOpcode::FIND_BY_TYPE_VALUE_REQUEST:
						start_characteristic_discovery(client);
						break;
					case AttributeOpcode::READ_BY_TYPE_REQUEST:
						handle_all_characteristics_discovered(client);
						break;
					default:
						// error
						break;
				}
			}   break;
			default:
				// error
				break;
		}
	}

	template<typename Response>
	void handle_service_discovered(GenericGattClient* client, const Response& response) {
		if (!response.size()) {
			terminate(client);
			return;
		}

		uint16_t end_handle = 0x0000;
		for (size_t i = 0; i < response.size(); ++i) {
			uint16_t start_handle = get_start_handle(response[i]);
			end_handle = get_end_handle(response[i]);
			UUID uuid = get_uuid(response[i]);

			if (!characteristic_callback) {
				DiscoveredService discovered_service;
				discovered_service.setup(uuid, start_handle, end_handle);
				service_callback(&discovered_service);
			} else {
				service_t* discovered_service = new (std::nothrow) service_t(
					start_handle, end_handle, uuid
				);

				if (discovered_service == NULL) {
					terminate(client);
					return;
				}

				insert_service(discovered_service);
			}
		}

		if (end_handle == 0xFFFF) {
			start_characteristic_discovery(client);
		} else {
			ble_error_t err = client->_pal_client->discover_primary_service(
				connection_handle, end_handle + 1
			);

			if (err) {
				terminate(client);
			}
		}
	}

	void start_characteristic_discovery(GenericGattClient* client) {
		if (!services_discovered) {
			terminate(client);
			return;
		}

		if (!characteristic_callback) {
			terminate(client);
			return;
		}

		if (service_callback) {
			DiscoveredService discovered_service;
			discovered_service.setup(
				services_discovered->uuid,
				services_discovered->begin,
				services_discovered->end
			);
			service_callback(&discovered_service);
		}

		last_characteristic = characteristic_t();
		client->_pal_client->discover_characteristics_of_a_service(
			connection_handle,
			attribute_handle_range(
				services_discovered->begin,
				services_discovered->end
			)
		);
	}

	void handle_characteristic_discovered(GenericGattClient* client, const AttReadByTypeResponse& response) {
		for (size_t i = 0; i < response.size(); ++i) {
			if (last_characteristic.is_valid() == false) {
				last_characteristic.set_last_handle(response[i].handle - 1);
				if (matching_characteristic_uuid == UUID()
				|| last_characteristic.getUUID() == matching_characteristic_uuid) {
					characteristic_callback(&last_characteristic);
				}
			}

			last_characteristic = characteristic_t(
				client, connection_handle, response[i].handle, response[i].value
			);
		}

		// check if all the characteristics of the service has been discovered
		if (last_characteristic.getValueHandle() == services_discovered->end) {
			handle_all_characteristics_discovered(client);
		} else {
			ble_error_t err = client->_pal_client->discover_characteristics_of_a_service(
				connection_handle,
				attribute_handle_range(
					last_characteristic.getValueHandle() + 1,
					services_discovered->end
				)
			);

			if (err) {
				terminate(client);
			}
		}
	}

	void handle_all_characteristics_discovered(GenericGattClient* client) {
		if (last_characteristic.is_valid() == false) {
			if (matching_characteristic_uuid == UUID()
				|| matching_characteristic_uuid == last_characteristic.getUUID()) {
				last_characteristic.set_last_handle(services_discovered->end);
				characteristic_callback(&last_characteristic);
			}
		}

		service_t* old = services_discovered;
		services_discovered = services_discovered->next;
		delete old;

		if (!services_discovered) {
			terminate(client);
		} else {
			start_characteristic_discovery(client);
		}
	}

	void terminate(GenericGattClient* client) {
		// unknown error, terminate the procedure immediately
		client->remove_control_block(this);
		Gap::Handle_t handle = connection_handle;
		delete this;
		client->on_termination(handle);
	}

	uint16_t get_start_handle(const AttReadByGroupTypeResponse::attribute_data_t& data) {
		return data.group_range.begin;
	}

	uint16_t get_start_handle(const attribute_handle_range_t& range) {
		return range.begin;
	}

	uint16_t get_end_handle(const AttReadByGroupTypeResponse::attribute_data_t& data) {
		return data.group_range.end;
	}

	uint16_t get_end_handle(const attribute_handle_range_t& range) {
		return range.end;
	}

	UUID get_uuid(const AttReadByGroupTypeResponse::attribute_data_t& data) {
		if (data.value.size() == 2) {
			return UUID(data.value[0] | data.value[1] << 8);
		} else {
			return UUID(data.value.data(), UUID::LSB);
		}
	}

	UUID get_uuid(const attribute_handle_range_t& range) {
		return matching_service_uuid;
	}

	struct service_t {
		service_t(uint16_t begin, uint16_t end, const UUID& uuid) :
			begin(begin), end(end), uuid(uuid), next(NULL) { }
		uint16_t begin;
		uint16_t end;
		UUID uuid;
		service_t* next;
	};

	struct characteristic_t : DiscoveredCharacteristic {
		characteristic_t() : DiscoveredCharacteristic() {
			lastHandle = 0x0001;
		}

		characteristic_t(
			GattClient* client,
			Gap::Handle_t connection_handle,
			uint16_t decl_handle,
			const ArrayView<const uint8_t> value
		) : DiscoveredCharacteristic() {
			gattc = client;
			uuid = get_uuid(value);
			props = get_properties(value);
			declHandle = decl_handle;
			valueHandle = get_value_handle(value);
			lastHandle = 0x0000;
			connHandle = connection_handle;
		}

		static UUID get_uuid(const ArrayView<const uint8_t>& value) {
			if (value.size() == 5) {
				return UUID(value[3] | (value[4] << 8));
			} else {
				return UUID(value.data() + 3, UUID::LSB);
			}
		}

		static DiscoveredCharacteristic::Properties_t get_properties(const ArrayView<const uint8_t>& value) {
			uint8_t raw_properties = value[0];
			DiscoveredCharacteristic::Properties_t result;
			result._broadcast = (raw_properties & (1 << 0)) ? true : false;
			result._read = (raw_properties & (1 << 1)) ? true : false;
			result._writeWoResp = (raw_properties & (1 << 2))  ? true : false;
			result._write = (raw_properties & (1 << 3))  ? true : false;
			result._notify = (raw_properties & (1 << 4))  ? true : false;
			result._indicate = (raw_properties & (1 << 5))  ? true : false;
			result._authSignedWrite = (raw_properties & (1 << 6))  ? true : false;
			return result;
		}

		static uint16_t get_value_handle(const ArrayView<const uint8_t>& value) {
			return value[1] | (value[2] << 8);
		}

		void set_last_handle(uint16_t last_handle) {
			lastHandle = last_handle;
		}

		bool is_valid() const {
			return lastHandle != 0x0000;
		}
	};

	void insert_service(service_t* service) {
		if (services_discovered == NULL) {
			services_discovered = service;
			return;
		}

		service_t* current = services_discovered;
		while (current->next) {
			current = current->next;
		}
		current->next = service;
	}

	ServiceDiscovery::ServiceCallback_t service_callback;
	ServiceDiscovery::CharacteristicCallback_t characteristic_callback;
	UUID matching_service_uuid;
	UUID matching_characteristic_uuid;
	service_t* services_discovered;
	characteristic_t last_characteristic;
	bool done;
};


struct read_control_block_t : public procedure_control_block_t {
	read_control_block_t(
		Gap::Handle_t connection_handle, uint16_t attribute_handle, uint16_t offset
	) : procedure_control_block_t(READ_PROCEDURE, connection_handle),
		attribute_handle(attribute_handle),
		offset(offset), current_offset(offset), data(NULL) {
	}

	virtual ~read_control_block_t() {
		if (data != NULL) {
			free(data);
		}
	}

	virtual void handle_timeout_error(GenericGattClient* client) {
		GattReadCallbackParams response = {
			connection_handle,
			attribute_handle,
			offset,
			0, // size of 0
			NULL, // no data
			BLE_ERROR_UNSPECIFIED,

		};
		terminate(client, response);
	}

	void terminate(GenericGattClient* client, const GattReadCallbackParams& response) {
		client->remove_control_block(this);
		client->processReadResponse(&response);
		delete this;
	}

	virtual void handle(GenericGattClient* client, const AttServerMessage& message) {
		switch(message.opcode) {
			case AttributeOpcode::ERROR_RESPONSE:
				handle_error(client, static_cast<const AttErrorResponse&>(message));
				break;

			case AttributeOpcode::READ_RESPONSE:
				handle_read_response(client, static_cast<const AttReadResponse&>(message));
				break;

			case AttributeOpcode::READ_BLOB_RESPONSE:
				handle_read_response(client, static_cast<const AttReadBlobResponse&>(message));
				break;

			default: {
				// should not happen, terminate the procedure and notify client with an error
				// in such case
				GattReadCallbackParams response = {
					connection_handle,
					attribute_handle,
					AttErrorResponse::UNLIKELY_ERROR,
					0, // size of 0
					NULL, // no data
					BLE_ERROR_UNSPECIFIED,

				};
				terminate(client, response);
			}   break;
		}
	}

	template<typename ResponseType>
	void handle_read_response(GenericGattClient* client, const ResponseType& read_response) {
		uint16_t mtu_size = client->get_mtu(connection_handle);

		// end of responses ?
		if ((uint16_t) read_response.size() < (mtu_size - 1)) {
			GattReadCallbackParams response = {
				connection_handle,
				attribute_handle,
				offset,
				0, // size of 0
				NULL, // no data
				BLE_ERROR_NONE,
			};

			// is it the first response, or is there any other response already
			// in the object ?
			if (data == NULL) {
				response.len = (uint16_t) read_response.size();
				response.data = read_response.data();
			} else {
				// copy the data in the existing buffer
				memcpy(data + (current_offset - offset), read_response.data(), read_response.size());
				response.len = (current_offset + read_response.size()) - offset;
				response.data = data;
			}
			terminate(client, response);
		} else {
			// allocation which will contain the response data plus the next one.
			data = (uint8_t*) realloc(data, (current_offset - offset) + ((mtu_size - 1) * 2));
			if (data == NULL) {
				GattReadCallbackParams response = {
					connection_handle,
					attribute_handle,
					offset,
					AttErrorResponse::INSUFFICIENT_RESOURCES,
					NULL,
					BLE_ERROR_NO_MEM,
				};
				terminate(client, response);
				return;
			}

			memcpy(data + (current_offset - offset), read_response.data(), read_response.size());
			current_offset = current_offset + read_response.size();
			ble_error_t err = client->_pal_client->read_attribute_blob(
				connection_handle,
				attribute_handle,
				current_offset
			);

			if (err) {
				GattReadCallbackParams response = {
					connection_handle,
					attribute_handle,
					AttErrorResponse::UNLIKELY_ERROR,
					0, // size of 0
					NULL, // no data
					BLE_ERROR_UNSPECIFIED,

				};
				terminate(client, response);
			}
		}
	}

	void handle_error(GenericGattClient* client, const AttErrorResponse& error) {
		ble_error_t status = BLE_ERROR_UNSPECIFIED;

		switch (error.error_code) {
			case AttErrorResponse::INVALID_HANDLE:
				status = BLE_ERROR_INVALID_PARAM;
				break;
			case AttErrorResponse::INSUFFICIENT_AUTHORIZATION:
				status = BLE_ERROR_INVALID_STATE;
				break;
			case AttErrorResponse::INSUFFICIENT_AUTHENTICATION:
				status = BLE_ERROR_INVALID_STATE;
				break;
			case AttErrorResponse::INSUFFICIENT_ENCRYPTION_KEY_SIZE:
				status = BLE_ERROR_INVALID_STATE;
				break;
			case AttErrorResponse::INSUFFICIENT_ENCRYPTION:
				status = BLE_ERROR_INVALID_STATE;
				break;
			case AttErrorResponse::READ_NOT_PERMITTED:
				status = BLE_ERROR_OPERATION_NOT_PERMITTED;
				break;
			case AttErrorResponse::INVALID_OFFSET:
				status = BLE_ERROR_PARAM_OUT_OF_RANGE;
				break;
			case AttErrorResponse::ATTRIBUTE_NOT_LONG:
				status = BLE_ERROR_PARAM_OUT_OF_RANGE;
				break;
			default:
				status = BLE_ERROR_UNSPECIFIED;
				break;
		}

		GattReadCallbackParams response = {
			connection_handle,
			attribute_handle,
			offset,
			error.error_code,
			/* data */ NULL,
			status
		};

		terminate(client, response);
	}

	uint16_t attribute_handle;
	uint16_t offset;
	uint16_t current_offset;
	uint8_t* data;
};

/*
 * Control block for the write process
 */
struct write_control_block_t : public procedure_control_block_t {
	write_control_block_t(
		Gap::Handle_t connection_handle, uint16_t attribute_handle,
		uint8_t* data, uint16_t len
	) : procedure_control_block_t(WRITE_PROCEDURE, connection_handle),
		attribute_handle(attribute_handle), len(len), offset(0), data(data),
		prepare_success(false), status(BLE_ERROR_UNSPECIFIED), error_code(0xFF) {
	}

	virtual ~write_control_block_t() {
		free(data);
	}

	virtual void handle_timeout_error(GenericGattClient* client) {
		GattWriteCallbackParams response = {
			connection_handle,
			attribute_handle,
			GattWriteCallbackParams::OP_WRITE_REQ,
			BLE_ERROR_UNSPECIFIED,
			0x00
		};
		terminate(client, response);
	}

	void terminate(GenericGattClient* client, const GattWriteCallbackParams& response) {
		client->remove_control_block(this);
		client->processWriteResponse(&response);
		delete this;
	}

	virtual void handle(GenericGattClient* client, const AttServerMessage& message) {
		switch(message.opcode) {
			case AttributeOpcode::ERROR_RESPONSE:
				handle_error(client, static_cast<const AttErrorResponse&>(message));
				break;

			case AttributeOpcode::WRITE_RESPONSE:
				handle_write_response(client, static_cast<const AttWriteResponse&>(message));
				break;

			case AttributeOpcode::PREPARE_WRITE_RESPONSE:
				handle_prepare_write_response(client, static_cast<const AttPrepareWriteResponse&>(message));
				break;

			case AttributeOpcode::EXECUTE_WRITE_RESPONSE:
				handle_execute_write_response(client, static_cast<const AttExecuteWriteResponse&>(message));
				break;

			default: {
				GattWriteCallbackParams response = {
					connection_handle,
					attribute_handle,
					GattWriteCallbackParams::OP_WRITE_REQ,
					BLE_ERROR_UNSPECIFIED,
					AttErrorResponse::UNLIKELY_ERROR
				};

				terminate(client, response);
			}   break;
		}
	}

	void handle_write_response(GenericGattClient* client, const AttWriteResponse& write_response) {
		GattWriteCallbackParams response = {
			connection_handle, attribute_handle,
			GattWriteCallbackParams::OP_WRITE_REQ,
			BLE_ERROR_NONE, 0x00
		};

		terminate(client, response);
	}

	void handle_prepare_write_response(GenericGattClient* client, const AttPrepareWriteResponse& write_response) {
		ble_error_t err = BLE_ERROR_UNSPECIFIED;

		uint16_t mtu_size = client->get_mtu(connection_handle);
		offset = write_response.offset + write_response.partial_value.size();
		if (offset < len) {
			err = client->_pal_client->queue_prepare_write(
				connection_handle, attribute_handle,
				make_const_ArrayView(
					data + offset,
					std::min((len - offset), (mtu_size - 5))
				),
				offset
			);
		} else {
			err = client->_pal_client->execute_write_queue(
				connection_handle, true
			);
		}

		if (err) {
			clear_prepare_queue(client, err, AttErrorResponse::UNLIKELY_ERROR);
		}
	}

	void handle_execute_write_response(GenericGattClient* client, const AttExecuteWriteResponse& execute_response) {
		if (prepare_success) {
			status = BLE_ERROR_NONE;
			error_code = 0x00;
		}

		GattWriteCallbackParams response = {
			connection_handle,
			attribute_handle,
			GattWriteCallbackParams::OP_WRITE_REQ,
			status,
			error_code
		};

		terminate(client, response);
	}

	void clear_prepare_queue(GenericGattClient* client, ble_error_t s, uint8_t e) {
		prepare_success = false;
		status = s;
		error_code = e;
		ble_error_t err = client->_pal_client->execute_write_queue(
			connection_handle, false
		);

		if (err) {
			GattWriteCallbackParams response = {
				connection_handle,
				attribute_handle,
				GattWriteCallbackParams::OP_WRITE_REQ,
				err,
				AttErrorResponse::UNLIKELY_ERROR
			};

			terminate(client, response);
		}
	}

	void handle_error(GenericGattClient* client, const AttErrorResponse& error) {
		ble_error_t status = BLE_ERROR_UNSPECIFIED;

		switch (error.error_code) {
			case AttErrorResponse::INVALID_HANDLE:
				status = BLE_ERROR_INVALID_PARAM;
				break;
			case AttErrorResponse::INVALID_ATTRIBUTE_VALUE_LENGTH:
				status = BLE_ERROR_INVALID_PARAM;
				break;
			case AttErrorResponse::INSUFFICIENT_AUTHORIZATION:
				status = BLE_ERROR_INVALID_STATE;
				break;
			case AttErrorResponse::INSUFFICIENT_AUTHENTICATION:
				status = BLE_ERROR_INVALID_STATE;
				break;
			case AttErrorResponse::INSUFFICIENT_ENCRYPTION_KEY_SIZE:
				status = BLE_ERROR_INVALID_STATE;
				break;
			case AttErrorResponse::INSUFFICIENT_ENCRYPTION:
				status = BLE_ERROR_INVALID_STATE;
				break;
			case AttErrorResponse::WRITE_NOT_PERMITTED:
				status = BLE_ERROR_OPERATION_NOT_PERMITTED;
				break;
			default:
				status = BLE_ERROR_UNSPECIFIED;
				break;
		}

		if (error.request_opcode == AttributeOpcode(AttributeOpcode::PREPARE_WRITE_REQUEST)) {
			clear_prepare_queue(client, status, error.error_code);
		} else {
			GattWriteCallbackParams response = {
				connection_handle,
				attribute_handle,
				GattWriteCallbackParams::OP_WRITE_REQ,
				status,
				error.error_code
			};

			terminate(client, response);
		}
	}

	uint16_t attribute_handle;
	uint16_t len;
	uint16_t offset;
	uint8_t* data;
	bool prepare_success;
	ble_error_t status;
	uint8_t error_code;
};

/*
 * Control block for the descriptor discovery process
 */
struct descriptor_discovery_control_block_t : public procedure_control_block_t {
	descriptor_discovery_control_block_t(
		const DiscoveredCharacteristic& characteristic,
		const CharacteristicDescriptorDiscovery::DiscoveryCallback_t& discoveryCallback,
		const CharacteristicDescriptorDiscovery::TerminationCallback_t& terminationCallback
	) : procedure_control_block_t(DESCRIPTOR_DISCOVERY_PROCEDURE, characteristic.getConnectionHandle()),
		characteristic(characteristic),
		discovery_cb(discoveryCallback),
		termination_cb(terminationCallback),
		next_handle(characteristic.getValueHandle() + 1),
		done(false) {
	}

	virtual ~descriptor_discovery_control_block_t() { }

	ble_error_t start(GenericGattClient* client) {
		return client->_pal_client->discover_characteristics_descriptors(
			connection_handle,
			attribute_handle_range(
				next_handle,
				characteristic.getLastHandle()
			)
		);
	}

	virtual void handle_timeout_error(GenericGattClient* client) {
		terminate(client, BLE_ERROR_UNSPECIFIED);
	}

	virtual void handle(GenericGattClient* client, const AttServerMessage& message) {
		if (done) {
			terminate(client, BLE_ERROR_NONE);
			return;
		}

		switch(message.opcode) {
			case AttributeOpcode::ERROR_RESPONSE:
				handle_error(client, static_cast<const AttErrorResponse&>(message));
				return;

			case AttributeOpcode::FIND_INFORMATION_RESPONSE:
				handle_response(client, static_cast<const AttFindInformationResponse&>(message));
				return;

			default:
				break;
		}
	}

	void handle_error(GenericGattClient* client, const AttErrorResponse& error) {
		if (error.error_code == AttErrorResponse::ATTRIBUTE_NOT_FOUND) {
			terminate(client, BLE_ERROR_NONE);
		} else {
			terminate(client, BLE_ERROR_UNSPECIFIED, error.error_code);
		}
	}

	void handle_response(GenericGattClient* client, const AttFindInformationResponse& response) {
		for (size_t i = 0; i < response.size(); ++i) {
			DiscoveredCharacteristicDescriptor descriptor(
				client, connection_handle, response[i].handle, response[i].uuid
			);
			CharacteristicDescriptorDiscovery::DiscoveryCallbackParams_t params = {
				characteristic,
				descriptor
			};
			discovery_cb(&params);
			if (done) {
				terminate(client, BLE_ERROR_NONE);
				return;
			}

			if (response[i].handle == characteristic.getLastHandle()) {
				terminate(client, BLE_ERROR_NONE);
				return;
			}
			next_handle = response[i].handle + 1;
		}

		ble_error_t err = start(client);
		if (err) {
			terminate(client, err, AttErrorResponse::UNLIKELY_ERROR);
		}
	}

	void terminate(GenericGattClient* client, ble_error_t status, uint8_t error_code = 0x00) {
		client->remove_control_block(this);
		CharacteristicDescriptorDiscovery::TerminationCallbackParams_t params = {
			characteristic,
			status,
			error_code
		};
		termination_cb(&params);
		delete this;
	}

	DiscoveredCharacteristic characteristic;
	CharacteristicDescriptorDiscovery::DiscoveryCallback_t discovery_cb;
	CharacteristicDescriptorDiscovery::TerminationCallback_t termination_cb;
	uint16_t next_handle;
	bool done;
};


GenericGattClient::GenericGattClient(pal::GattClient* pal_client) :
	_pal_client(pal_client),
	_termination_callback(),
	 control_blocks(NULL) {
	_pal_client->when_server_message_received(
		mbed::callback(this, &GenericGattClient::on_server_message_received)
	);
	_pal_client->when_transaction_timeout(
		mbed::callback(this, &GenericGattClient::on_transaction_timeout)
	);
}

ble_error_t GenericGattClient::launchServiceDiscovery(
	Gap::Handle_t connection_handle,
	ServiceDiscovery::ServiceCallback_t service_callback,
	ServiceDiscovery::CharacteristicCallback_t characteristic_callback,
	const UUID& matching_service_uuid,
	const UUID& matching_characteristic_uuid
) {
	// verify that there is no other procedures going on this connection
	if (get_control_block(connection_handle)) {
		return BLE_ERROR_INVALID_STATE;
	}

	// terminate and return if there is no callback to call
	if (!service_callback && !characteristic_callback) {
		on_termination(connection_handle);
		return BLE_ERROR_NONE;
	}

	discovery_control_block_t* discovery_pcb = new(std::nothrow) discovery_control_block_t(
		connection_handle,
		service_callback,
		characteristic_callback,
		matching_service_uuid,
		matching_characteristic_uuid
	);

	if (discovery_pcb == NULL) {
		return BLE_ERROR_NO_MEM;
	}

	// note: control block inserted prior the request because they are part of
	// of the transaction and the callback can be call synchronously
	insert_control_block(discovery_pcb);

	// launch the request
	ble_error_t err = BLE_ERROR_UNSPECIFIED;
	if (matching_service_uuid == UUID()) {
		err = _pal_client->discover_primary_service(
			connection_handle,
			0x0001
		);
	} else {
		err = _pal_client->discover_primary_service_by_service_uuid(
			connection_handle,
			0x0001,
			matching_service_uuid
		);
	}

	if (err) {
		remove_control_block(discovery_pcb);
		delete discovery_pcb;
	}

	return err;
}

bool GenericGattClient::isServiceDiscoveryActive() const {
	procedure_control_block_t* pcb = control_blocks;

	while (pcb) {
		if (pcb->type == COMPLETE_DISCOVERY_PROCEDURE) {
			return true;
		}
		pcb = pcb->next;
	}

	return false;
}

void GenericGattClient::terminateServiceDiscovery()
{
	procedure_control_block_t* pcb = control_blocks;
	while (pcb) {
		if (pcb->type == COMPLETE_DISCOVERY_PROCEDURE) {
			static_cast<discovery_control_block_t*>(pcb)->done = true;
		}
		pcb = pcb->next;
	}
}

ble_error_t GenericGattClient::read(
	Gap::Handle_t connection_handle,
	GattAttribute::Handle_t attribute_handle,
	uint16_t offset) const
{
	// verify that there is no other procedures going on this connection
	if (get_control_block(connection_handle)) {
		return BLE_ERROR_INVALID_STATE;
	}

	read_control_block_t* read_pcb = new(std::nothrow) read_control_block_t(
		connection_handle,
		attribute_handle,
		offset
	);

	if (read_pcb == NULL) {
		return BLE_ERROR_NO_MEM;
	}

	insert_control_block(read_pcb);

	ble_error_t err = BLE_ERROR_NONE;

	if (offset == 0) {
		err = _pal_client->read_attribute_value(
			connection_handle, attribute_handle
		);
	} else {
		err = _pal_client->read_attribute_blob(
			connection_handle, attribute_handle, offset
		);
	}

	if (err) {
		remove_control_block(read_pcb);
		delete read_pcb;
	}

	return err;
}

ble_error_t GenericGattClient::write(
	GattClient::WriteOp_t cmd,
	Gap::Handle_t connection_handle,
	GattAttribute::Handle_t attribute_handle,
	size_t length,
	const uint8_t* value
) const {
	// verify that there is no other procedures going on this connection
	if (get_control_block(connection_handle)) {
		return BLE_ERROR_INVALID_STATE;
	}

	uint16_t mtu = get_mtu(connection_handle);

	if (cmd == GattClient::GATT_OP_WRITE_CMD) {
		if (length > (uint16_t)(mtu - 3)) {
			return BLE_ERROR_PARAM_OUT_OF_RANGE;
		}
		return _pal_client->write_without_response(
			connection_handle,
			attribute_handle,
			make_const_ArrayView(value, length)
		);
	} else {
		uint8_t* data = NULL;

		if (length > (uint16_t)(mtu - 3)) {
			data = (uint8_t*) malloc(length);
			if (data == NULL) {
				return BLE_ERROR_NO_MEM;
			}
			memcpy(data, value, length);
		}

		write_control_block_t* write_pcb = new(std::nothrow) write_control_block_t(
			connection_handle,
			attribute_handle,
			data,
			length
		);

		if (write_pcb == NULL) {
			free(data);
			return BLE_ERROR_NO_MEM;
		}

		insert_control_block(write_pcb);

		ble_error_t err = BLE_ERROR_UNSPECIFIED;
		if (data) {
			err = _pal_client->queue_prepare_write(
				connection_handle,
				attribute_handle,
				make_const_ArrayView(value, mtu - 5),
				/* offset */ 0
			);
		} else {
			err = _pal_client->write_attribute(
				connection_handle,
				attribute_handle,
				make_const_ArrayView(value, length)
			);
		}

		if (err) {
			remove_control_block(write_pcb);
			delete write_pcb;
		}

		return err;
	}

	return BLE_ERROR_NOT_IMPLEMENTED;
}

void GenericGattClient::onServiceDiscoveryTermination(
	ServiceDiscovery::TerminationCallback_t callback
) {
	_termination_callback = callback;
}

ble_error_t GenericGattClient::discoverCharacteristicDescriptors(
	const DiscoveredCharacteristic& characteristic,
	const CharacteristicDescriptorDiscovery::DiscoveryCallback_t& discoveryCallback,
	const CharacteristicDescriptorDiscovery::TerminationCallback_t& terminationCallback
) {
	// verify that there is no other procedures going on this connection
	if (get_control_block(characteristic.getConnectionHandle())) {
		return BLE_ERROR_INVALID_STATE;
	}

	if (characteristic.getValueHandle() == characteristic.getLastHandle()) {
		CharacteristicDescriptorDiscovery::TerminationCallbackParams_t params = {
			characteristic,
			BLE_ERROR_NONE,
			/* error code */ 0x00
		};

		terminationCallback(&params);
		return BLE_ERROR_NONE;
	}

	descriptor_discovery_control_block_t* discovery_pcb =
		new(std::nothrow) descriptor_discovery_control_block_t(
			characteristic,
			discoveryCallback,
			terminationCallback
		);

	if (discovery_pcb == NULL) {
		return BLE_ERROR_NO_MEM;
	}

	insert_control_block(discovery_pcb);

	ble_error_t err = discovery_pcb->start(this);

	if (err) {
		remove_control_block(discovery_pcb);
		delete discovery_pcb;
	}

	return err;
}

bool GenericGattClient::isCharacteristicDescriptorDiscoveryActive(
	const DiscoveredCharacteristic& characteristic
) const {
	procedure_control_block_t* pcb = control_blocks;

	while (pcb) {
		if (pcb->type == DESCRIPTOR_DISCOVERY_PROCEDURE &&
			static_cast<descriptor_discovery_control_block_t*>(pcb)->characteristic == characteristic) {
			return true;
		}
		pcb = pcb->next;
	}

	return false;
}

void GenericGattClient::terminateCharacteristicDescriptorDiscovery(
	const DiscoveredCharacteristic& characteristic
) {
	procedure_control_block_t* pcb = control_blocks;

	while (pcb) {
		if (pcb->type == DESCRIPTOR_DISCOVERY_PROCEDURE) {
			descriptor_discovery_control_block_t* dpcb =
				static_cast<descriptor_discovery_control_block_t*>(pcb);
			if (dpcb->characteristic == characteristic) {
				dpcb->done = true;
				return;
			}
		}

		pcb = pcb->next;
	}

}

ble_error_t GenericGattClient::reset(void) {
	return BLE_ERROR_NOT_IMPLEMENTED;
}

void GenericGattClient::on_termination(Gap::Handle_t connection_handle) {
	if (_termination_callback) {
		_termination_callback(connection_handle);
	}
}

void GenericGattClient::on_server_message_received(
	connection_handle_t connection_handle,
	const AttServerMessage& message
) {
	switch(message.opcode) {
		case AttributeOpcode::ERROR_RESPONSE:
		case AttributeOpcode::EXCHANGE_MTU_RESPONSE:
		case AttributeOpcode::FIND_INFORMATION_RESPONSE:
		case AttributeOpcode::FIND_BY_VALUE_TYPE_RESPONSE:
		case AttributeOpcode::READ_BY_TYPE_RESPONSE:
		case AttributeOpcode::READ_RESPONSE:
		case AttributeOpcode::READ_BLOB_RESPONSE:
		case AttributeOpcode::READ_MULTIPLE_RESPONSE:
		case AttributeOpcode::READ_BY_GROUP_TYPE_RESPONSE:
		case AttributeOpcode::WRITE_RESPONSE:
		case AttributeOpcode::PREPARE_WRITE_RESPONSE:
		case AttributeOpcode::EXECUTE_WRITE_RESPONSE: {
			on_server_response(connection_handle, message);
		} break;

		case AttributeOpcode::HANDLE_VALUE_INDICATION:
		case AttributeOpcode::HANDLE_VALUE_NOTIFICATION: {
			on_server_event(connection_handle, message);
		} break;

		default:
			// invalid message receive
			return;
	}
}

void GenericGattClient::on_server_response(
	connection_handle_t connection,
	const AttServerMessage& message
) {
	procedure_control_block_t* pcb = get_control_block(connection);
	if (pcb == NULL) {
		return;
	}

	pcb->handle(this, message);
}

void GenericGattClient::on_server_event(connection_handle_t connection, const AttServerMessage& message) {
	GattHVXCallbackParams callbacks_params = {
		(Gap::Handle_t) connection, 0
	};

	switch (message.opcode) {
		case AttributeOpcode::HANDLE_VALUE_NOTIFICATION: {
			const AttHandleValueNotification& notification =
				static_cast<const AttHandleValueNotification&>(message);
			callbacks_params.handle = notification.attribute_handle;
			callbacks_params.type = BLE_HVX_NOTIFICATION;
			callbacks_params.len = notification.attribute_value.size();
			callbacks_params.data = notification.attribute_value.data();
		} break;

		case AttributeOpcode::HANDLE_VALUE_INDICATION: {
			const AttHandleValueIndication& indication =
				static_cast<const AttHandleValueIndication&>(message);
			callbacks_params.handle = indication.attribute_handle;
			callbacks_params.type = BLE_HVX_INDICATION;
			callbacks_params.len = indication.attribute_value.size();
			callbacks_params.data = indication.attribute_value.data();
		} break;

		default:
			return;
	}

	processHVXEvent(&callbacks_params);
}

void GenericGattClient::on_transaction_timeout(connection_handle_t connection) {
	procedure_control_block_t* pcb = get_control_block(connection);
	if (pcb == NULL) {
		return;
	}

	pcb->handle_timeout_error(this);
}

procedure_control_block_t* GenericGattClient::get_control_block(Gap::Handle_t connection) {
	procedure_control_block_t* it = control_blocks;
	while (it && it->connection_handle != connection) {
		it = it->next;
	}
	return it;
}

const procedure_control_block_t* GenericGattClient::get_control_block(Gap::Handle_t connection) const {
	procedure_control_block_t* it = control_blocks;
	while (it && it->connection_handle != connection) {
		it = it->next;
	}
	return it;
}

void GenericGattClient::insert_control_block(procedure_control_block_t* cb) const {
	if (control_blocks == NULL) {
		control_blocks = cb;
		return;
	}

	procedure_control_block_t* current = control_blocks;
	while (current->next) {
		current = current->next;
	}
	current->next = cb;
}

void GenericGattClient::remove_control_block(procedure_control_block_t* cb) const {
	if (control_blocks == NULL) {
		return;
	}

	if (cb == control_blocks) {
		control_blocks = control_blocks->next;
		return;
	}

	procedure_control_block_t* current = control_blocks;
	while (current->next && current->next != cb) {
		current = current->next;
	}

	if (current->next == NULL) {
		return;
	}

	current->next = cb->next;
	cb->next = NULL;
}

uint16_t GenericGattClient::get_mtu(Gap::Handle_t connection) const {
	uint16_t result = 23;
	if(_pal_client->get_mtu_size((connection_handle_t) connection, result) != BLE_ERROR_NONE) {
		result = 23;
	}
	return result;
}

} // namespace pal
} // namespace ble
