// intellectual property is bullshit bgdc

#ifndef SERIAL_AVRDX_HPP
#define SERIAL_AVRDX_HPP

#include <stdbool.h>
#include <stddef.h>
#include <inttypes.h>
#include <avrstuff/serial.hpp>
#include <avrstuff/math_misc.hpp>

// Serial interface class for AVR-Dx USART. Does not provide initialization,
// the user should do this first. If using a character size of 9 bits, 9BITH
// (high byte first) must be used.
//
// NUsart: number of the usart
// Chsize: number of bits per character
template<int NUsart, int Chsize>
class serial_avrdx : public serial
{
public:
	virtual ~serial_avrdx() = default;

	virtual int recv() override
	{
		uint8_t hi = Usart()->RXDATAH;
		uint8_t lo = Usart()->RXDATAL;

		uint8_t flags = hi & (USART_PERR_bm | USART_FERR_bm
				| USART_BUFOVF_bm | USART_RXCIF_bm);

		if (flags & USART_FERR_bm)
			return serial::FERR;
		else if (flags & USART_PERR_bm)
			return serial::PERR;
		else if (flags & USART_BUFOVF_bm)
			return serial::BUF_OVERFLOW;
		else if (flags == USART_RXCIF_bm && Chsize > 8)
			return ((hi & 0x01) << 8) | lo;
		else if (flags == USART_RXCIF_bm && Chsize <= 8)
			return lo;
		else
			return EOF;
	}

	virtual void send(int b) override
	{
		while (!(Usart()->STATUS & USART_DREIF_bm));
		if (Chsize > 8)
			Usart()->TXDATAH = (b & 0x100) ? 0x1 : 0;
		Usart()->TXDATAL = (uint8_t) b;
	}

	virtual void rxcie(bool en) override
	{
		if (en)
			Usart()->CTRLA |= USART_RXCIE_bm;
		else
			Usart()->CTRLA &= ~USART_RXCIE_bm;
	}

	virtual void txcie(bool en) override
	{
		if (en)
			Usart()->CTRLA |= USART_TXCIE_bm;
		else
			Usart()->CTRLA &= ~USART_TXCIE_bm;
	}

	virtual void dreie(bool en) override
	{
		if (en)
			Usart()->CTRLA |= USART_DREIE_bm;
		else
			Usart()->CTRLA &= ~USART_DREIE_bm;
	}

	// Set the baud rate.
	//
	// Return: true if the baud rate could be set
	constexpr bool set_baud(uint32_t baud)
	{
		uint16_t breg_normal =
			rounded_div(64uLL * F_CPU, 16uLL * baud);
		uint16_t breg_2x =
			rounded_div(64uLL * F_CPU, 8uLL * baud);

		int32_t baud_normal =
			rounded_div(64LL * F_CPU, 16LL * breg_normal);
		int32_t baud_2x =
			rounded_div(64LL * F_CPU, 16LL * breg_2x);

		int32_t error_normal = baud_normal - baud;
		int32_t error_2x = baud_2x - baud;

		bool valid_normal = error_normal > -(int32_t)baud/100 &&
			error_normal < (int32_t)baud/100;
		bool valid_2x = error_2x > -(int32_t)baud/100 &&
			error_2x < (int32_t)baud/100;

		if (valid_normal)
		{
			Usart()->BAUD = breg_normal;
			Usart()->CTRLB = (Usart()->CTRLB & ~USART_RXMODE_gm)
				| USART_RXMODE_NORMAL_gc;
			return true;
		}
		else if (valid_2x)
		{
			Usart()->BAUD = breg_2x;
			Usart()->CTRLB = (Usart()->CTRLB & ~USART_RXMODE_gm)
				| USART_RXMODE_CLK2X_gc;
			return true;
		}
		else
			return false;
	}

	// Stop the USART. Interrupts and the transceiver are disabled. Other
	// settings may be lost, so it should be restarted with start().
	void stop()
	{
		Usart()->CTRLA = 0;
		Usart()->CTRLB &= ~(USART_RXEN_bm | USART_TXEN_bm);
	}

	// Start the USART. You should first set the baud rate with set_baud()
	void start(bool rxen, bool txen, char parity, bool twostop)
	{
		Usart()->CTRLA = 0;

		Usart()->CTRLC =
			USART_CMODE_ASYNCHRONOUS_gc |
			(parity == 'E' ? USART_PMODE_EVEN_gc :
			 parity == 'O' ? USART_PMODE_ODD_gc :
			 USART_PMODE_DISABLED_gc) |
			(twostop ? USART_SBMODE_bm : 0) |
			(Chsize == 9 ? USART_CHSIZE_9BITH_gc :
			 Chsize == 8 ? USART_CHSIZE_8BIT_gc :
			 Chsize == 7 ? USART_CHSIZE_7BIT_gc :
			 Chsize == 6 ? USART_CHSIZE_6BIT_gc :
			 USART_CHSIZE_5BIT_gc);

		Usart()->CTRLB |=
			(rxen ? USART_RXEN_bm : 0) |
			(txen ? USART_TXEN_bm : 0);
	}

	USART_t * Usart() const {
		return
#ifdef USART0
			NUsart == 0 ? &USART0 :
#endif
#ifdef USART1
			NUsart == 1 ? &USART1 :
#endif
#ifdef USART2
			NUsart == 2 ? &USART2 :
#endif
#ifdef USART3
			NUsart == 3 ? &USART3 :
#endif
#ifdef USART4
			NUsart == 4 ? &USART4 :
#endif
#ifdef USART5
			NUsart == 5 ? &USART5 :
#endif
			NULL;
	}
};

#endif // !defined(SERIAL_AVRDX_HPP)
