#include "s600_iap.h"
#include "s600_can.h"
#include "string.h"

static u8 s600_iap_page[FMC_PAGE_SIZE];
static u32 s600_iap_address;
static u16 s600_iap_length;

static void s600_iap_flag_clear(void)
{
#ifdef CONFIG_GD32E10X
    fmc_flag_clear(FMC_FLAG_PGERR | FMC_FLAG_PGAERR | FMC_FLAG_WPERR | FMC_FLAG_END);
#else
	fmc_flag_clear(FMC_FLAG_BANK0_PGERR | FMC_FLAG_BANK0_WPERR | FMC_FLAG_BANK0_END);
#endif
}

static u32 s600_iap_capacity(void)
{
	return (REG32(0x1FFFF7E0) & 0xFFFF) << 10;
}

static u32 s600_iap_last_page(void)
{
	u32 capacity = s600_iap_capacity();
	return 0x08000000 + (capacity - FMC_PAGE_SIZE);
}

void s600_iap_write_begin(void)
{
	s600_iap_address = PART_APP;
	s600_iap_length = 0;
}

bool s600_iap_write_end(u32 length, u32 checksum)
{
	u32 buff[2];

	if (!s600_iap_flush(s600_iap_length)) {
		return false;
	}

	if (checksum != s600_iap_checksum(PART_APP, length)) {
		return false;
	}

	buff[0] = length;
	buff[1] = checksum;

	return s600_iap_write_page(s600_iap_last_page(), buff, sizeof(buff), true);
}

bool s600_iap_write_page(u32 addr, const void *buff, u16 length, bool erase)
{
	const u32 *p, *p_end;

	fmc_unlock();

	if (erase) {
		s600_iap_flag_clear();

		if (FMC_READY != fmc_page_erase(addr)) {
			return false;
		}
	}

	for (p = (const u32 *) buff, p_end = p + ((length + 3) / 4); p < p_end; p++, addr += 4) {
		s600_iap_flag_clear();

		if (FMC_READY != fmc_word_program(addr, *p)) {
			return false;
		}
	}

	fmc_lock();

	return true;
}

bool s600_iap_flush(u32 length)
{
	if (!s600_iap_write_page(s600_iap_address, s600_iap_page, length, true)) {
		return false;
	}

	s600_iap_address += length;
	s600_iap_length = 0;

	return true;
}

bool s600_iap_write(const u8 *buff, u16 length)
{
	while (1) {
		u16 remain = sizeof(s600_iap_page) - s600_iap_length;

		if (length < remain) {
			memcpy(s600_iap_page + s600_iap_length, buff, length);
			s600_iap_length += length;
			break;
		}

		memcpy(s600_iap_page + s600_iap_length, buff, remain);
		if (!s600_iap_flush(sizeof(s600_iap_page))) {
			return false;
		}

		buff += remain;
		length -= remain;
	}

	return true;
}

bool s600_iap_write_u32(u32 value)
{
	return s600_iap_write((u8 *) &value, sizeof(value));
}

#if S600_IAP_CRC32
u32 s600_iap_checksum(u32 addr, u32 length)
{
	u32 crc = 0xFFFFFFFFU;
	const u8 *p, *p_end;

	for (p = (u8 *) addr, p_end = p + length; p < p_end; p++) {
		u8 value = *p;
		u8 i;

		for (i = 0; i < 8; i++) {
			if (((crc ^ value) & 1) != 0) {
				crc = (crc >> 1) ^ 0xEDB88320U;
			} else {
				crc >>= 1;
			}

			value >>= 1;
		}
	}

	return crc ^ 0xFFFFFFFFU;
}
#else
u32 s600_iap_checksum(u32 addr, u32 length)
{
	const u8 *p, *p_end;
	u32 checksum = 0;

	for (p = (u8 *) addr, p_end = p + length; p < p_end; p++) {
		checksum += *p;
	}

	return checksum;
}
#endif

bool s600_iap_boot(bool normal)
{
	const s600_iap_info_t *info = (s600_iap_info_t *) PART_FACTORY;
	const char *board_name = (const char *) PART_NAME;

	if (info->success == 0xFFFFFFFF) {
		u32 value = APP_BOOT_OK;
		s600_iap_write_page((u32) &info->success, &value, sizeof(value), false);
		s600_iap_write_page(s600_iap_last_page(), info, sizeof(*info), true);
	}

	if (strcmp(board_name, CONFIG_BOARD_NAME) != 0) {
		return false;
	}

	info = (s600_iap_info_t *) s600_iap_last_page();

	if (normal) {
		if (info->success != APP_BOOT_OK) {
			return false;
		}
	} else if (info->length > s600_iap_capacity() - 0x2000) {
		return false;
	} else if (info->checksum != s600_iap_checksum(PART_APP, info->length)) {
		return false;
	}

#if 0
	if ((REG32(PART_APP) & 0x2FFE0000) != 0x20000000) {
		return false;
	}
#endif

#ifdef CONFIG_BOARD_PS100
	nvic_irq_disable(USART0_IRQn);
	nvic_irq_disable(USART1_IRQn);
#endif

	s600_can_device_deinit();
	SysTick->CTRL = 0;

	/* initialize user application's stack pointer */
	__set_MSP(REG32(PART_APP));
	nvic_vector_table_set(PART_APP, 0);

	/* jump to user application */
	((void (*)(void)) REG32(PART_APP + 4))();

	return false;
}
