#include "communication_interface.h"

#ifdef COMM_MQTT

#ifdef ARDUINO
	#include <wolfMQTT.h>
#else /* ARDUINO */
	#include <wolfmqtt/mqtt_client.h>
#endif /* ARDUINO */

#include "mem.h"

#define INTERNAL_BUFFER_SIZE 256
#define EXTERNAL_BUFFER_SIZE 256

#define PING_TRANSMISSION_TIME KEEP_ALIVE_TIME/2

// Ring buffer
struct RingBuffer {
	int head;
	int tail;
	byte* buf;
};

int rc = 0;
uint16_t packet_id = 0;
MqttNet net;
MqttClient mqtt_client;

struct RingBuffer recv_buf;
int send_buf_idx = -1;
byte send_buf[EXTERNAL_BUFFER_SIZE];

uint16_t keep_alive_time = 0;
unsigned long lastping = 0;
char client_id[24];

bool comm_continuation = false;

static struct CommSettings comm_settings =
	{ .clean_session=CLEAN_SESSION
	, .keep_alive_time=KEEP_ALIVE_TIME
	, .client_id=CLIENT_ID};
struct CommSettings get_comm_settings()
{
	return comm_settings;
}

// Ring buffer helpers
void init_ringbuf(struct RingBuffer* rb) {
	memset(rb, 0, sizeof(struct RingBuffer));
	rb->buf = malloc(EXTERNAL_BUFFER_SIZE * sizeof(byte));

	if (rb->buf == NULL) {
		die("Memory error");
	}
}

byte read_ringbuf(struct RingBuffer* rb) {

	if (rb->head == rb->tail) {
		die("Read on empty ring buffer");
	}

	byte out = rb->buf[rb->tail];
	rb->tail = (rb->tail + 1) % EXTERNAL_BUFFER_SIZE;
	return out;
}

void write_ringbuf(struct RingBuffer* rb, byte b) {

	if (rb->head == (rb->tail - 1)) {
		die("Ring buffer full");
	}

	rb->buf[rb->head] = b;
	rb->head = (rb->head + 1) % EXTERNAL_BUFFER_SIZE;
}

void process_send_buf()
{
	packet_id = packet_id + 1;

	// Topic
	char topic[26];
	strcpy(topic, "up/");
	strcat(topic, client_id);

	// Create packet
	MqttPublish publish;
	memset(&publish, 0, sizeof(MqttPublish));
	publish.qos = 1;
	publish.topic_name = topic;
	publish.packet_id = packet_id;
	publish.buffer = send_buf;
	publish.total_len = send_buf_idx + 1;


	if (MqttClient_Publish(&mqtt_client, &publish)) {
		die("MQTT publish: %s", MqttClient_ReturnCodeToString(rc));
	}

	send_buf_idx = -1;
}

void process_recv_buf()
{
	// Check for incoming messages
	if (link_input_available()) {
		rc = MqttClient_WaitMessage(&mqtt_client, 0);

		if (!(rc == MQTT_CODE_ERROR_TIMEOUT || rc == 0)) {
			die("MQTT receive: %s",
				MqttClient_ReturnCodeToString(rc));
		}
	}
}

void generate_client_id()
{
	const char* alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXY"
		"Zabcdefghijklmnopqrstuvwxyz";
	client_id[0] = 'd';
	unsigned int rand;

	for (int i = 1; i < 23; i++) {
		rand = get_random();
		client_id[i] = alphabet[rand % 62];
	}

	client_id[23] = '\0';
}

bool input_available(void)
{
	communication_yield();
	return recv_buf.head != recv_buf.tail;
}

uint8_t read_byte(void) {

	while (!input_available())
		msdelay(500);

	byte in = read_ringbuf(&recv_buf);
	return in;
}

void write_byte(uint8_t b)
{

	if (send_buf_idx == EXTERNAL_BUFFER_SIZE-1) {
		die("Buffer full");
	}

	send_buf[++send_buf_idx] = b;
}

struct MTMessageTo receive_message(void)
{
	return parse_MTMessageTo(read_byte, mem_alloc_task);
}

void send_message(struct MTMessageFro r)
{
	print_MTMessageFro(write_byte, r);
	process_send_buf();
}

static int mqtt_message_cb(
	MqttClient *client,
	MqttMessage *msg,
	byte msg_new,
	byte msg_done)
{
	(void) *client;
	(void) msg_new;
	(void) msg_done;

	for (unsigned int i = 0; i < msg->buffer_len; i++) {
		write_ringbuf(&recv_buf, msg->buffer[i]);
	}

	return MQTT_CODE_SUCCESS;
}

static int read_cb(
	void *context,
	byte* buf,
	int buf_len,
	int timeout_ms)
{

	(void) context;
	(void) timeout_ms;

	int i = 0;
	for (; i < buf_len; i++) {

		// Wait for input
		while (!link_input_available()) {
			msdelay(50);
		}

		buf[i] = link_read_byte();
	}

	return i;
}

static int write_cb(
	void *context,
	const byte* buf,
	int buf_len,
	int timeout_ms)
{
	(void) context;
	(void) timeout_ms;

	for (int i = 0; i < buf_len; i++) {
		link_write_byte(buf[i]);
	}

	return buf_len;
}

static int connect_cb(
	void *context,
	const char* host,
	word16 port,
	int timeout_ms)
{

	open_link((struct LinkSettings)
		{ .serverMode=false, .port=port, .host=host });
	return MQTT_CODE_SUCCESS;
	(void) context;
	(void) timeout_ms;
}

static int disconnect_cb(void *context)
{
	(void) context;

	// Unexpected disconnect
	if (!comm_continuation) {
		reset();
	}

	close_link(comm_continuation);
	return MQTT_CODE_SUCCESS;
}

void start_communication(struct CommSettings cs, struct LinkSettings ls)
{
	if (!comm_continuation) {
		comm_settings = cs;

		// Validate settings
		if (cs.keep_alive_time < PING_TRANSMISSION_TIME) {
			die("Keep alive time must be greater than %d seconds\n",
				PING_TRANSMISSION_TIME);
		}

		init_ringbuf(&recv_buf);

		memset(&net, 0, sizeof(net));
		net.connect = connect_cb;
		net.read = read_cb;
		net.write = write_cb;
		net.disconnect = disconnect_cb;

		byte *txBuf = NULL;
		byte *rxBuf = NULL;

		txBuf = (byte*)malloc(INTERNAL_BUFFER_SIZE);
		rxBuf = (byte*)malloc(INTERNAL_BUFFER_SIZE);

		if (txBuf == NULL || rxBuf == NULL) {
			die("Memory error");
		}

		// Get the client id
		if (!cs.client_id) {
			generate_client_id();
		} else {
			strncpy(client_id, cs.client_id, 24);
			client_id[23] = '\0';
		}

		rc = MqttClient_Init(&mqtt_client, &net, mqtt_message_cb,
			 txBuf, INTERNAL_BUFFER_SIZE, rxBuf, INTERNAL_BUFFER_SIZE,
			 1000);

		if (rc != MQTT_CODE_SUCCESS) {
			die("MQTT init: %s", MqttClient_ReturnCodeToString(rc));
		}
	}

	// Create connection
	rc = MqttClient_NetConnect(&mqtt_client, ls.host, ls.port,
		1000, false, NULL);

	if (rc != MQTT_CODE_SUCCESS) {
		die("MQTT connect: %s", MqttClient_ReturnCodeToString(rc));
	}

	// Connect
	MqttConnect connect;
	memset(&connect, 0, sizeof(connect));
	connect.keep_alive_sec = cs.keep_alive_time;
	connect.client_id = client_id;
	connect.clean_session = cs.clean_session && !comm_continuation;
	connect.username = NULL;
	connect.password = NULL;

	// LWT
	MqttMessage lwt_msg;
	memset(&lwt_msg, 0, sizeof(lwt_msg));

	connect.lwt_msg = &lwt_msg;
	connect.enable_lwt = true;

	lwt_msg.qos = 1;
	lwt_msg.retain = false;

	// Topic
	char lwt_topic[26];
	strcpy(lwt_topic, "up/");
	strcat(lwt_topic, client_id);

	lwt_msg.topic_name = (char*) lwt_topic;

	// Create the will
	print_MTMessageFro(write_byte, (struct MTMessageFro)
		{ .cons=MTFException_c
		, .data={.MTFException=
			{.cons=MTEUnexpectedDisconnect_c }
		}});

	lwt_msg.buffer = send_buf;
	lwt_msg.total_len = send_buf_idx + 1;

	send_buf_idx = -1;

	keep_alive_time = cs.keep_alive_time;
	lastping = getmillis();

	rc = MqttClient_Connect(&mqtt_client, &connect);

	// Check if the request was successful
	if (rc != 0) {
		die("MQTT connect: %s", MqttClient_ReturnCodeToString(rc));
	}

	// Check if the request was accepted
	switch (connect.ack.return_code) {
	case 1:
		die("Incorrect MQTT protocol version");
		break;
	case 2:
	case 3:
		die("Connection refused");
		break;
	case 4:
	case 5:
		die("Auth failed");
		break;
	};

	if (comm_continuation) {
		comm_continuation = false;
		return;
	}

	MqttSubscribe subscribe;
	MqttTopic topics[1];

	// Topic
	char topic[28];
	strcpy(topic, "down/");
	strcat(topic, client_id);

	topics[0].topic_filter = topic;
	topics[0].qos = 1;

	memset(&subscribe, 0, sizeof(subscribe));
	subscribe.packet_id = ++packet_id;
	subscribe.topic_count = 1;
	subscribe.topics = topics;
	rc = MqttClient_Subscribe(&mqtt_client, &subscribe);

	if (rc != MQTT_CODE_SUCCESS) {
		die("MQTT subscribe: %s", MqttClient_ReturnCodeToString(rc));
	}

#ifdef HAVE_OLEDSHIELD
	clear_display();
	print_to_display("client id:\n");
	print_to_display(client_id);
	print_to_display("\n");
	flush_display();
#endif /* HAVE_OLEDSHIELD */

	msg_log(SC("Client id: %s\n"), client_id);
}

void stop_communication(bool temporary)
{
	comm_continuation = temporary;

	MqttClient_Disconnect(&mqtt_client);
	MqttClient_NetDisconnect(&mqtt_client);

	if (!temporary) {
		free(recv_buf.buf);
		free(mqtt_client.rx_buf);
		free(mqtt_client.tx_buf);
		MqttClient_DeInit(&mqtt_client);
	}
}

void cleanup_communication()
{
	cleanup_link();
}

void communication_yield(void)
{
	process_recv_buf();

	unsigned long treshhold = (keep_alive_time - PING_TRANSMISSION_TIME) * 1000;

	if (keep_alive_time != 0 && getmillis()-lastping > treshhold) {
		lastping = getmillis();
		MqttClient_Ping(&mqtt_client);

		if (rc != MQTT_CODE_SUCCESS) {
			die("MQTT ping: %s", MqttClient_ReturnCodeToString(rc));
		}
	}
}
#endif /* COMM_MQTT */
