/* Reflow Oven Controller
*
* Copyright (C) 2020  Mario Hüttel <mario.huettel@gmx.net>
*
* This file is part of the Reflow Oven Controller Project.
*
* The reflow oven controller is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* The Reflow Oven Control Firmware is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with the reflow oven controller project.
* If not, see <http://www.gnu.org/licenses/>.
*/

#include <reflow-controller/safety/safety-memory.h>
#include <helper-macros/helper-macros.h>
#include <stm-periph/crc-unit.h>
#include <stm-periph/backup-ram.h>
#include <base64-lib/base64-lib.h>

static int word_to_error_memory_entry(uint32_t entry_data, struct error_memory_entry *out)
{
	int ret = 0;

	if (!out)
		return -1002;

	if (entry_data == SAFETY_MEMORY_ERR_ENTRY_NOP) {
		out->flag_num = 0U;
		out->type = SAFETY_MEMORY_ERR_ENTRY_NOP;
		out->counter = 0U;
	} else if ((entry_data & 0xFFU) == 0x51U) {
		out->flag_num = (uint8_t)((entry_data >> 8U) & 0xFFU);
		out->type = SAFETY_MEMORY_ERR_ENTRY_FLAG;
		out->counter = (uint16_t)((entry_data >> 16U) & 0xFFFF);
	} else {
		/* Invalid entry */
		ret = -1;
	}

	return ret;
}

static uint32_t error_memory_entry_to_word(const struct error_memory_entry *entry)
{
	uint32_t word = 0;

	switch (entry->type) {
	case SAFETY_MEMORY_ERR_ENTRY_NOP:
		word = SAFETY_MEMORY_NOP_ENTRY;
		break;
	case SAFETY_MEMORY_ERR_ENTRY_FLAG:
		word = 0x51UL | ((uint32_t)entry->flag_num << 8U) |
				((uint32_t)entry->counter << 16U);
		break;
	}

	return word;
}

static enum safety_memory_state safety_memory_get_header(struct safety_memory_header *header)
{

	int res;
	enum safety_memory_state ret;

	if (!header)
		return SAFETY_MEMORY_INIT_CORRUPTED;

	res = backup_ram_get_data(0UL, (uint32_t *)header, wordsize_of(struct safety_memory_header));
	if (res)
		return SAFETY_MEMORY_INIT_CORRUPTED;

	/* Check magic */
	if (header->magic != SAFETY_MEMORY_MAGIC) {
		/* Magic invalid */
		ret = SAFETY_MEMORY_INIT_FRESH;
		goto return_val;
	}

	/* Check the header crc */
	crc_unit_reset();
	crc_unit_input_array((uint32_t *)header, wordsize_of(struct safety_memory_header));
	if (crc_unit_get_crc() != 0UL) {
		ret = SAFETY_MEMORY_INIT_CORRUPTED;
		goto return_val;
	}

	res = 0;
	if (header->boot_status_offset < wordsize_of(struct safety_memory_header))
		res++;
	if (header->config_overrides_offset < header->boot_status_offset + wordsize_of(struct safety_memory_boot_status))
		res++;
	if (header->config_overrides_len > SAFETY_MEMORY_CONFIG_OVERRIDE_COUNT)
		res++;
	if (header->err_memory_offset < header->config_overrides_offset + header->config_overrides_len)
		res++;
	if (header->err_memory_end >= backup_ram_get_size_in_words() || header->err_memory_end < header->err_memory_offset)
		res++;

	if (res) {
		/* Error detected: Write new header */
		ret = SAFETY_MEMORY_INIT_CORRUPTED;
	} else {
		ret = SAFETY_MEMORY_INIT_VALID_MEMORY;
	}

return_val:
	return ret;
}

static void safety_memory_write_and_patch_header(struct safety_memory_header *header)
{
	/* Patch the CRC */
	crc_unit_reset();
	crc_unit_input_array((uint32_t *)header, wordsize_of(struct safety_memory_header) - 1U);
	header->crc = crc_unit_get_crc();

	/* Write to memory */
	backup_ram_write_data(0UL, (uint32_t *)header, wordsize_of(*header));
}

static void safety_memory_write_new_header(void)
{
	struct safety_memory_header header;

	header.boot_status_offset = wordsize_of(struct safety_memory_header);
	header.config_overrides_len = SAFETY_MEMORY_CONFIG_OVERRIDE_COUNT;
	header.config_overrides_offset = header.boot_status_offset + wordsize_of(struct safety_memory_boot_status);
	header.err_memory_offset = header.config_overrides_offset + SAFETY_MEMORY_CONFIG_OVERRIDE_COUNT;
	header.err_memory_end = header.err_memory_offset;
	header.magic = SAFETY_MEMORY_MAGIC;

	backup_ram_wipe();
	safety_memory_write_and_patch_header(&header);
}

static int safety_memory_check_crc()
{
	struct safety_memory_header header;
	enum safety_memory_state state = safety_memory_get_header(&header);
	uint32_t crc_offset;
	uint32_t data;
	uint32_t addr;
	int res;

	if (state != SAFETY_MEMORY_INIT_VALID_MEMORY)
		return -1;

	crc_offset = header.err_memory_end;

	crc_unit_reset();

	for (addr = 0; addr < crc_offset; addr++) {
		res = backup_ram_get_data(addr, &data, 1UL);
		if (res)
			return -2000;
		crc_unit_input(data);
	}

	res = backup_ram_get_data(crc_offset, &data, 1UL);
	if (res)
		return -2001;

	if (crc_unit_get_crc() != data)
		return -3000;
	else
		return 0;
}

static int safety_memory_gen_crc()
{
	struct safety_memory_header header;
	uint32_t word_addr;
	uint32_t data;
	int res;

	if (safety_memory_get_header(&header) != SAFETY_MEMORY_INIT_VALID_MEMORY)
		return -1;

	crc_unit_reset();
	for (word_addr = 0; word_addr < header.err_memory_end; word_addr++) {
		res = backup_ram_get_data(word_addr, &data, 1);
		if (res)
			return -2;
		crc_unit_input(data);
	}

	/* Write CRC */
	data = crc_unit_get_crc();
	res = backup_ram_write_data(header.err_memory_end, &data, 1UL);
	if (res)
		return -3;

	return 0;
}

int safety_memory_reinit(enum safety_memory_state *found_state)
{
	struct safety_memory_header header;
	int res;
	int ret = -1;

	if (!found_state)
		return -1001;

	*found_state = safety_memory_get_header(&header);

	switch (*found_state) {
	case SAFETY_MEMORY_INIT_VALID_MEMORY:
		/* Valid memory detected. Check CRC and error entries */
		res = safety_memory_check();
		if (res)
			*found_state = SAFETY_MEMORY_INIT_CORRUPTED;
		break;
	case SAFETY_MEMORY_INIT_FRESH:
		break;
	case SAFETY_MEMORY_INIT_CORRUPTED:
		break;
	default:
		*found_state = SAFETY_MEMORY_INIT_CORRUPTED;
		break;
	}

	/* Check if memory header has to be written */
	if (*found_state != SAFETY_MEMORY_INIT_VALID_MEMORY) {
		safety_memory_write_new_header();
		/* If yes, generate new CRC checksum */
		res = safety_memory_gen_crc();
		if (res)
			ret = -100;
		else
			ret = 0;
	} else {
		ret = 0;
	}

	return ret;
}

int safety_memory_init(enum safety_memory_state *found_state)
{

	crc_unit_init();
	backup_ram_init(true);

	return safety_memory_reinit(found_state);
}

int safety_memory_get_boot_status(struct safety_memory_boot_status *status)
{
	struct safety_memory_header header;
	int res;

	if (!status)
		return -1001;

	if (safety_memory_get_header(&header) != SAFETY_MEMORY_INIT_VALID_MEMORY) {
		return -2000;
	}

	if (safety_memory_check_crc())
		return -2001;

	res = backup_ram_get_data(header.boot_status_offset, (uint32_t *)status, wordsize_of(*status));
	if (res)
		return -3000;
	return 0;
}

int safety_memory_set_boot_status(const struct safety_memory_boot_status *status)
{
	struct safety_memory_header header;
	int res;

	if (!status)
		return -1001;

	if (safety_memory_get_header(&header) != SAFETY_MEMORY_INIT_VALID_MEMORY) {
		return -2000;
	}

	if (safety_memory_check_crc())
		return -2001;

	res = backup_ram_write_data(header.boot_status_offset, (uint32_t *)status, wordsize_of(*status));
	res |= safety_memory_gen_crc();
	if (res)
		return -3000;

	return 0;
}

static int safety_memory_check_error_entries()
{
	struct safety_memory_header header;
	uint32_t addr;
	uint32_t data;
	int ret = 0;
	int res;

	if (safety_memory_get_header(&header) != SAFETY_MEMORY_INIT_VALID_MEMORY) {
		return -2000;
	}

	for (addr = header.err_memory_offset; addr < header.err_memory_end; addr++) {
		res = backup_ram_get_data(addr, &data, 1UL);
		if (res)
			return -100;

		/* Valid flag entry */
		if ((data & 0xFF) == 0x51)
			continue;
		if (data == SAFETY_MEMORY_NOP_ENTRY)
			continue;

		ret--;
	}

	return ret;
}

int safety_memory_get_error_entry_count(uint32_t *count)
{
	struct safety_memory_header header;

	if (!count)
		return -1001;

	if (safety_memory_get_header(&header) != SAFETY_MEMORY_INIT_VALID_MEMORY) {
		return -2000;
	}

	*count = header.err_memory_end - header.err_memory_offset;

	return 0;
}

int safety_memory_check(void)
{
	int res;

	res = safety_memory_check_crc();
	if (!res) {
		res |= safety_memory_check_error_entries();
	}

	return -!!res;
}

int safety_memory_get_error_entry(uint32_t idx, struct error_memory_entry *entry)
{
	struct safety_memory_header header;
	uint32_t err_mem_count;
	int ret = -1;
	int res;
	uint32_t data;

	if (!entry)
		return -1001;

	if (safety_memory_get_header(&header) != SAFETY_MEMORY_INIT_VALID_MEMORY) {
		return -2000;
	}

	err_mem_count = header.err_memory_end - header.err_memory_offset;
	if (idx < err_mem_count && err_mem_count > 0) {
		res = backup_ram_get_data(header.err_memory_offset + idx, &data, 1UL);
		if (res)
			goto return_value;

		res = word_to_error_memory_entry(data, entry);
		if (res)
			goto return_value;
		ret = 0;

	} else {
		/* out of range */
		ret = -1001;
	}

return_value:
	return ret;
}

int safety_memory_insert_error_entry(struct error_memory_entry *entry)
{
	int res;
	int ret = -0xFFFF;
	uint32_t addr;
	uint32_t data;
	bool found;
	uint32_t input_data;
	struct error_memory_entry current_entry;

	struct safety_memory_header header;

	input_data = error_memory_entry_to_word(entry);

	if (safety_memory_get_header(&header) != SAFETY_MEMORY_INIT_VALID_MEMORY) {
		return -2000;
	}

	if (entry->type == SAFETY_MEMORY_ERR_ENTRY_NOP) {
		/* Append to end */
		if ((header.err_memory_end + 1U) < backup_ram_get_size_in_words()) {
			/* Still fits in memory */
			backup_ram_write_data(header.err_memory_end, &input_data, 1UL);
			header.err_memory_end++;
			safety_memory_write_and_patch_header(&header);
			safety_memory_gen_crc();
			ret = 0;
		}
	} else if (entry->type == SAFETY_MEMORY_ERR_ENTRY_FLAG) {
		found = false;
		for (addr = header.err_memory_offset; addr < header.err_memory_end; addr++) {
			res = backup_ram_get_data(addr, &data, 1UL);
			if (res) {
				ret = -1;
				goto return_value;
			}
			res = word_to_error_memory_entry(data, &current_entry);
			if (res) {
				ret = -2;
				goto return_value;
			}

			if (current_entry.type == SAFETY_MEMORY_ERR_ENTRY_FLAG &&
					current_entry.flag_num == entry->flag_num) {
				found = true;
				break;
			}

			if (current_entry.type == SAFETY_MEMORY_ERR_ENTRY_NOP) {
				found = true;
				break;
			}
		}

		if (!found) {
			/* No suitable place found in memory. Append */

			if ((addr + 1) < backup_ram_get_size_in_words()) {
				backup_ram_write_data(addr, &input_data, 1UL);
				header.err_memory_end++;
				safety_memory_write_and_patch_header(&header);
			} else {
				ret = -3;
				goto return_value;
			}
		} else {
			if (current_entry.type == SAFETY_MEMORY_ERR_ENTRY_NOP) {
				backup_ram_write_data(addr, &input_data, 1UL);
			} else {
				current_entry.counter += entry->counter;
				if (current_entry.counter < entry->counter)
					current_entry.counter = 0xFFFF;
				data = error_memory_entry_to_word(&current_entry);
				backup_ram_write_data(addr, &data, 1UL);
			}
		}

		safety_memory_gen_crc();
		ret = 0;

	} else {
		ret = -1001;
	}

return_value:
	return ret;
}


static uint32_t convert_config_override_to_word(const struct config_override *conf_override)
{
	uint32_t data = 0;

	if (conf_override->type == SAFETY_MEMORY_CONFIG_OVERRIDE_WEIGHT) {
		data |= 0xAA0000A2UL;
		data |= ((uint32_t)conf_override->entry.weight_override.flag) << 16;
		data |= ((uint32_t)conf_override->entry.weight_override.weight) << 8;
	} else if (conf_override->type == SAFETY_MEMORY_CONFIG_OVERRIDE_PERSISTENCE) {
		data |= 0xBB00008EUL;
		data |= ((uint32_t)conf_override->entry.persistence_override.flag) << 16;
		data |= ((uint32_t)(conf_override->entry.persistence_override.persistence ? 1UL : 0UL)) << 8;
	}

	return data;
}

int safety_memory_insert_config_override(struct config_override *config_override)
{
	struct safety_memory_header header;
	uint32_t idx;
	uint32_t data;
	int res;
	int ret = -3;

	if (safety_memory_get_header(&header) != SAFETY_MEMORY_INIT_VALID_MEMORY) {
		return -2000;
	}

	if (header.config_overrides_len == 0)
		return -1;

	for (idx = 0; idx < header.config_overrides_len; idx++) {
		res = backup_ram_get_data(header.config_overrides_offset + idx, &data, 1UL);
		if (res)
			return -2;
		if (data == 0UL) {
			data = convert_config_override_to_word(config_override);
			res = backup_ram_write_data(header.config_overrides_offset + idx, &data, 1UL);
			if (res)
				return -4;
			res = safety_memory_gen_crc();
			if (res)
				return -5;
			ret = 0;
			break;
		}
	}

	return ret;
}

int safety_memory_get_config_override_count(uint32_t *count)
{
	struct safety_memory_header header;
	uint32_t iter;
	uint32_t valid_count;
	uint32_t data;
	int res;

	if (!count)
		return -1001;

	*count = 0UL;

	if (safety_memory_get_header(&header) != SAFETY_MEMORY_INIT_VALID_MEMORY) {
		return -2000;
	}

	if (header.config_overrides_len == 0)
		return 0;

	valid_count = 0;
	for (iter = 0; iter < header.config_overrides_len; iter++) {
		res = backup_ram_get_data(header.config_overrides_offset + iter, &data, 1UL);
		if (res)
			return -2;

		if (data != 0)
			valid_count++;
		else
			break;
	}

	*count = valid_count;
	return 0;
}

int safety_memory_get_config_override(uint32_t idx, struct config_override *config_override)
{
	struct safety_memory_header header;
	uint32_t data;
	int res;

	if (!config_override)
		return -1002;

	if (safety_memory_get_header(&header) != SAFETY_MEMORY_INIT_VALID_MEMORY) {
		return -2000;
	}

	if (idx >= header.config_overrides_len) {
		return -1001;
	}

	res = backup_ram_get_data(header.config_overrides_offset + idx, &data, 1UL);
	if (res) {
		return -1;
	}

	switch (data & 0xFF) {
	case 0xA2:
		/*  Weight override */
		config_override->type = SAFETY_MEMORY_CONFIG_OVERRIDE_WEIGHT;
		config_override->entry.weight_override.flag = (data & 0xFF0000UL) >> 16;
		config_override->entry.weight_override.weight = (data & 0xFF00UL) >> 8;
		break;
	case 0x8E:
		/* persistence override */
		config_override->type = SAFETY_MEMORY_CONFIG_OVERRIDE_PERSISTENCE;
		config_override->entry.persistence_override.flag = (data & 0xFF0000UL) >> 16;
		config_override->entry.persistence_override.persistence = ((data & 0xFF00UL) >> 8) ? true : false;
		break;
	default:
		return -2;
	}

	return 0;
}

int safety_memory_dump_base64(char *buffer, size_t buffsize, size_t *used_size)
{
	uint32_t safety_mem_size;
	size_t output_size;
	const char *backup_mem_ptr;
	int res;

	if (!buffer)
		return -1000;

	safety_mem_size = backup_ram_get_size_in_words() * 4U;
	output_size = base64_calculate_encoded_size(safety_mem_size);

	if (output_size + 1 > buffsize)
		return -1001;

	backup_mem_ptr = (const char *)backup_ram_get_base_ptr();
	res = base64_encode(backup_mem_ptr, buffer, safety_mem_size, buffsize, &output_size);
	if (res)
		return -1;
	buffer[output_size] = '\0';
	if (used_size)
		*used_size = output_size + 1u;
	return 0;
}
