#include <stdio.h>
#include <string.h>

#include "stm32f407xx_esp8266.h"
#include "ringBuffer.h"

#define ESP8266_COMMAND_BUFFER_SIZE   256U
#define ESP8266_RESPONSE_BUFFER_SIZE  512U

extern ringBuffer_t bufferU3;

UART_HandleTypeDef hesp8266_uart;

static void ESP8266_DrainReceiveBuffer(void) {
	uint8_t data;

	while (RingBuff_Read(&bufferU3, &data) == 0U) {
	}
}

static HAL_StatusTypeDef ESP8266_WaitForReply(const char *expected_reply_1,
		const char *expected_reply_2, uint32_t timeout_ms) {
	static char response[ESP8266_RESPONSE_BUFFER_SIZE];
	size_t response_length = 0U;
	uint32_t tick_start = HAL_GetTick();

	response[0] = '\0';

	while ((HAL_GetTick() - tick_start) < timeout_ms) {
		uint8_t data;

		while (RingBuff_Read(&bufferU3, &data) == 0U) {
			if (response_length < (sizeof(response) - 1U)) {
				response[response_length++] = (char) data;
				response[response_length] = '\0';
			}

			if (((expected_reply_1 != NULL)
					&& (strstr(response, expected_reply_1) != NULL))
					|| ((expected_reply_2 != NULL)
							&& (strstr(response, expected_reply_2) != NULL))) {
				return HAL_OK;
			}

			if ((strstr(response, "ERROR") != NULL)
					|| (strstr(response, "FAIL") != NULL)) {
				return HAL_ERROR;
			}
		}

		HAL_Delay(1U);
	}

	return HAL_TIMEOUT;
}

static HAL_StatusTypeDef ESP8266_Command(const char *command,
		const char *expected_reply_1, const char *expected_reply_2,
		uint32_t timeout_ms) {
	HAL_StatusTypeDef status;
	static const uint8_t line_ending[] = "\r\n";

	if (command == NULL) {
		return HAL_ERROR;
	}

	ESP8266_DrainReceiveBuffer();

	status = HAL_UART_Transmit(&hesp8266_uart, (uint8_t*) command,
			(uint16_t) strlen(command), timeout_ms);
	if (status != HAL_OK) {
		return status;
	}

	status = HAL_UART_Transmit(&hesp8266_uart, (uint8_t*) line_ending,
			(uint16_t) (sizeof(line_ending) - 1U), timeout_ms);
	if (status != HAL_OK) {
		return status;
	}

	return ESP8266_WaitForReply(expected_reply_1, expected_reply_2, timeout_ms);
}

static void ESP8266_GPIO_Init(void) {
	GPIO_InitTypeDef GPIO_InitStruct = { 0 };

	__HAL_RCC_GPIOD_CLK_ENABLE();

	GPIO_InitStruct.Pin = ESP8266_RST_PIN;
	GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
	GPIO_InitStruct.Pull = GPIO_PULLUP;
	GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
	HAL_GPIO_Init(ESP8266_RST_GPIO_PORT, &GPIO_InitStruct);

	// Keep reset inactive while the UART peripheral is configured
	HAL_GPIO_WritePin(ESP8266_RST_GPIO_PORT, ESP8266_RST_PIN, GPIO_PIN_SET);
}

static HAL_StatusTypeDef ESP8266_UART_Init(void) {
	GPIO_InitTypeDef GPIO_InitStruct = { 0 };

	__HAL_RCC_USART3_CLK_ENABLE();
	__HAL_RCC_GPIOD_CLK_ENABLE();

	GPIO_InitStruct.Pin = GPIO_PIN_8 | GPIO_PIN_9;
	GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
	GPIO_InitStruct.Pull = GPIO_PULLUP;
	GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
	GPIO_InitStruct.Alternate = GPIO_AF7_USART3;
	HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);

	hesp8266_uart.Instance = ESP8266_UART_INSTANCE;
	hesp8266_uart.Init.BaudRate = ESP8266_UART_BAUD_RATE;
	hesp8266_uart.Init.WordLength = UART_WORDLENGTH_8B;
	hesp8266_uart.Init.StopBits = UART_STOPBITS_1;
	hesp8266_uart.Init.Parity = UART_PARITY_NONE;
	hesp8266_uart.Init.Mode = UART_MODE_TX_RX;
	hesp8266_uart.Init.HwFlowCtl = UART_HWCONTROL_NONE;
	hesp8266_uart.Init.OverSampling = UART_OVERSAMPLING_16;

	return HAL_UART_Init(&hesp8266_uart);
}

void ESP8266_Reset(void) {
	HAL_GPIO_WritePin(ESP8266_RST_GPIO_PORT, ESP8266_RST_PIN, GPIO_PIN_RESET);
	HAL_Delay(100);
	HAL_GPIO_WritePin(ESP8266_RST_GPIO_PORT, ESP8266_RST_PIN, GPIO_PIN_SET);
	HAL_Delay(500);
}

HAL_StatusTypeDef ESP8266_Init(void) {
	ESP8266_GPIO_Init();

	if (ESP8266_UART_Init() != HAL_OK) {
		return HAL_ERROR;
	}

	ESP8266_Reset();
	return HAL_OK;
}

HAL_StatusTypeDef ESP8266_Test(uint32_t attempts) {
	uint32_t attempt;

	for (attempt = 0U; attempt < attempts; attempt++) {
		if (ESP8266_Command("AT", "OK", NULL, 1000U) == HAL_OK) {
			return HAL_OK;
		}
		HAL_Delay(500U);
	}

	return HAL_TIMEOUT;
}

HAL_StatusTypeDef ESP8266_SetStationMode(void) {
	return ESP8266_Command("AT+CWMODE=1", "OK", "no change", 3000U);
}

HAL_StatusTypeDef ESP8266_JoinAccessPoint(const char *ssid,
		const char *password) {
	char command[ESP8266_COMMAND_BUFFER_SIZE];
	int command_length;

	if ((ssid == NULL) || (password == NULL)) {
		return HAL_ERROR;
	}

	command_length = snprintf(command, sizeof(command),
			"AT+CWJAP=\"%s\",\"%s\"", ssid, password);
	if ((command_length < 0) || ((size_t) command_length >= sizeof(command))) {
		return HAL_ERROR;
	}

	return ESP8266_Command(command, "OK", NULL, 20000U);
}

HAL_StatusTypeDef ESP8266_SetSingleConnection(void) {
	return ESP8266_Command("AT+CIPMUX=0", "OK", NULL, 2000U);
}

HAL_StatusTypeDef ESP8266_ConnectTcp(const char *server_ip,
		uint16_t server_port) {
	char command[ESP8266_COMMAND_BUFFER_SIZE];
	int command_length;

	if (server_ip == NULL) {
		return HAL_ERROR;
	}

	command_length = snprintf(command, sizeof(command),
			"AT+CIPSTART=\"TCP\",\"%s\",%u", server_ip,
			(unsigned int) server_port);
	if ((command_length < 0) || ((size_t) command_length >= sizeof(command))) {
		return HAL_ERROR;
	}

	/* "ALREADY CONNECTED" also contains the accepted CONNECT token. */
	return ESP8266_Command(command, "CONNECT", "OK", 10000U);
}

HAL_StatusTypeDef ESP8266_EnterTransparentMode(void) {
	HAL_StatusTypeDef status;

	status = ESP8266_Command("AT+CIPMODE=1", "OK", NULL, 2000U);
	if (status != HAL_OK) {
		return status;
	}

	/* AT+CIPSEND first reports OK, then '>' when transparent data mode is
	 * actually ready.  Do not accept the intermediate OK: payload sent before
	 * the prompt is parsed as an AT command and produces ERROR. */
	return ESP8266_Command("AT+CIPSEND", ">", NULL, 5000U);
}

HAL_StatusTypeDef ESP8266_ExitTransparentMode(void) {
	static const uint8_t escape_sequence[] = "+++";
	HAL_StatusTypeDef status;

	/* ESP8266 AT firmware requires a guard interval around the escape sequence. */
	HAL_Delay(1100U);
	status = HAL_UART_Transmit(&hesp8266_uart, (uint8_t*) escape_sequence,
			(uint16_t) (sizeof(escape_sequence) - 1U), 1000U);
	HAL_Delay(1100U);
	if (status != HAL_OK) {
		return status;
	}

	return ESP8266_Command("AT", "OK", NULL, 1000U);
}

HAL_StatusTypeDef ESP8266_Transmit(const uint8_t *data, uint16_t length,
		uint32_t timeout_ms) {
	if ((data == NULL) || (length == 0U)) {
		return HAL_ERROR;
	}

	return HAL_UART_Transmit(&hesp8266_uart, (uint8_t*) data, length,
			timeout_ms);
}

uint8_t ESP8266_ReadByte(uint8_t *data) {
	return RingBuff_Read(&bufferU3, data);
}
