/**
 * \file
 * \brief USB DFU bootloader implementation (DFU mode)
 *
 * Copyright (c) 2018-2019 sysmocom -s.f.m.c. GmbH, Author: Kevin Redon <kredon@sysmocom.de>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include <errno.h>
#include <stdio.h>
#include "atmel_start.h"
#include "atmel_start_pins.h"
#include "hpl_user_area.h"

/** Start address of the application in flash
 *  \remark must be initialized by check_bootloader
 */
static uint32_t *application_start_address = (uint32_t *)(BL_SIZE_BYTE);

/** Location of the DFU magic value to force starting DFU */
static volatile uint32_t *dfu_magic = (uint32_t *)HSRAM_ADDR; // magic value should be written at start of RAM

/** Check if starting the bootloader is forced
 *  \return true of the DFU bootloader should be started
 */
static bool check_force_dfu(void)
{
	if (0x44465521 == *dfu_magic) { // check for the magic value which can be set by the main application
		*dfu_magic = 0; // erase value so we don't stay in the DFU bootloader upon reset
		return true;
	}
	if (0 == gpio_get_pin_level(BUTTON_FORCE_DFU)) { // signal is low when button is pressed
		return true;
	}
	return false;
}

/** Check if the application is valid
 *  \return true if the application is valid and can be started
 *  \warning application_start_address must be initialized
 */
static bool check_application(void)
{
	/* the application starts with the vector table
	 * the first entry in the vector table is the initial stack pointer (SP) address
	 * the stack will be placed in RAM which begins at 0x2000 0000, and there is up to 256 KB of RAM (0x40000).
	 * if the SP is not in this range (e.g. flash has been erased) there is no valid application
	 * the second entry in the vector table is the reset address, corresponding to the application start
	 */
	return (HSRAM_ADDR == ((*application_start_address) & 0xFFF80000));
}

/** Start the application
 *  \warning application_start_address must be initialized
 */
static void start_application(void)
{
	__set_MSP(*application_start_address); // re-base the Stack Pointer
	SCB->VTOR =
		((uint32_t)application_start_address & SCB_VTOR_TBLOFF_Msk); // re-base the vector table base address
	__DSB();
	__ISB();
	asm("bx %0" ::"r"(*(application_start_address + 1))); // jump to application Reset Handler in the application */
}
#if defined(SYSMOOCTSIM)
/* Section 9.6 of SAMD5x/E5x Family Data Sheet */
static int get_chip_unique_serial(uint8_t *out, size_t len)
{
	uint32_t *out32 = (uint32_t *)out;
	if (len < 16)
		return -EINVAL;

	out32[0] = *(uint32_t *)0x008061fc;
	out32[1] = *(uint32_t *)0x00806010;
	out32[2] = *(uint32_t *)0x00806014;
	out32[3] = *(uint32_t *)0x00806018;

	return 0;
}

/* 4 bit nibble to chr */
static char nib_to_chr(uint8_t nib)
{
	nib &= 0xf;
	if (nib < 10)
		return nib + '0';
	else
		return nib - 10 + 'a';
}

/* same as get_chip_unique_serial but in hex-string format */
static int get_chip_unique_serial_str(char *out, size_t len)
{
	uint8_t buf[16];
	int rc;

	if (len < 16 * 2 + 1)
		return -EINVAL;

	rc = get_chip_unique_serial(buf, sizeof(buf));
	if (rc < 0)
		return rc;

	for (int i = 0; i < sizeof(buf) && (2 * i) + 1 < len; i++) {
		out[2 * i] = nib_to_chr(buf[i] >> 4);
		out[(2 * i) + 1] = nib_to_chr(buf[i]);
	}
	return 0;
}

static int str_to_usb_desc(char *in, uint8_t in_sz, uint8_t *out, uint8_t out_sz)
{
	if (2 + in_sz * 2 < out_sz)
		return -1;

	memset(out, 0, out_sz);
	out[0] = out_sz;
	out[1] = 0x3;
	for (int i = 2; i < out_sz; i += 2)
		out[i] = in[(i >> 1) - 1];
	return 0;
}

char sernr_buf[16 * 2 + 1];
char product_buf[] = "sysmoOCTSIM DFU-" GIT_VERSION;
uint8_t __attribute__((aligned(16))) sernr_buf_descr[1 + 1 + 16 * 2 * 2];
uint8_t __attribute__((aligned(16))) product_buf_descr[1 + 1 + (sizeof(product_buf) - 1) * 2];
#endif

int main(void)
{
	atmel_start_init(); // initialise system
#if defined(SYSMOOCTSIM)
	get_chip_unique_serial_str(sernr_buf, sizeof(sernr_buf));
	str_to_usb_desc(sernr_buf, sizeof(sernr_buf), sernr_buf_descr, sizeof(sernr_buf_descr));
	str_to_usb_desc(product_buf, sizeof(product_buf) - 1, product_buf_descr, sizeof(product_buf_descr));
#endif

	// errata 2.6.10, do not remove this, ever.
	bool chiprev_lower_revG = ((DSU->DID.reg >> 8) & 0xf) < 0x6;
	bool startup_wdt_inactive = _user_area_read_bits((void *)NVMCTRL_USER, 62, 1) != 1;
	if (chiprev_lower_revG && startup_wdt_inactive) {
		_user_area_write_bits((void *)NVMCTRL_USER, 50, 0, 4);
		_user_area_write_bits((void *)NVMCTRL_USER, 62, 1, 1);
	}

	// set bootprot bits for (15-13)=2 x8192 byte
	// hri_nvmctrl_write_CTRLB_reg(NVMCTRL, NVMCTRL_CTRLB_CMD_SBPDIS | NVMCTRL_CTRLB_CMDEX_KEY);
	while (!hri_nvmctrl_get_STATUS_READY_bit(NVMCTRL)) {
	}
	if (hri_nvmctrl_read_STATUS_BOOTPROT_bf(FLASH_0.dev.hw) != 13) {
		_user_area_write_bits((void *)NVMCTRL_FUSES_BOOTPROT_ADDR, NVMCTRL_FUSES_BOOTPROT_Pos, 13, 4);
	}

	if (!check_force_dfu() && check_application()) { // application is valid
		start_application(); // start application
	} else {
		if (!check_application()) { // if the application is corrupted the start DFU start should be dfuERROR
			dfu_state = USB_DFU_STATE_DFU_ERROR;
		}
		usb_init();
		usb_dfu(); // start DFU bootloader
	}
}
