// intellectual property is bullshit bgdc

#ifndef SPI_AVRDX_HPP
#define SPI_AVRDX_HPP

#include <stdbool.h>
#include <stddef.h>
#include <inttypes.h>
#include <avrstuff/spi.hpp>
#include <avr/io.h>

template<int NSpi>
class spi_avrdx: public spi
{
public:
	enum class clockdiv: uint8_t
	{
		div2   = SPI_PRESC_DIV4_gc  | SPI_CLK2X_bm,
		div4   = SPI_PRESC_DIV4_gc,
		div8   = SPI_PRESC_DIV16_gc | SPI_CLK2X_bm,
		div16  = SPI_PRESC_DIV16_gc,
		div32  = SPI_PRESC_DIV64_gc | SPI_CLK2X_bm,
		div64  = SPI_PRESC_DIV64_gc,
		div128 = SPI_PRESC_DIV128_gc,
	};

	virtual ~spi_avrdx() = default;

	virtual uint8_t transfer(uint8_t data) override
	{
		transfer_block_u8(&data, &data, 1);
		return data;
	}

	void transfer_block_u8(
		uint8_t const * data_out,
		uint8_t       * data_in,
		uint8_t         length
	)
	{
		Spi()->INTFLAGS = 0xFF;

		if (!data_in)
		{
			// This loop can maintain a 93% transmit duty cycle
			// at div/2
			for (uint8_t i = 0; i < length; i++)
			{
				while (!(Spi()->INTFLAGS & SPI_DREIF_bm));
				Spi()->DATA = data_out[i];
			}
		}
		else
		{
			// This loop can maintain a 58% transmit duty cycle
			// at div/2. It's not a fully saturated link, but it's
			// better than 50% (so higher throughput than running
			// one divider lower). If there is any further
			// optimization to be had, it's in the middle loop.

			uint8_t i_out = 0, i_in = 0, discard = 2;
			while (i_out < length && discard)
			{
				uint8_t flags = Spi()->INTFLAGS;

				if (flags & (SPI_RXCIF_bm | SPI_BUFOVF_bm | SPI_TXCIF_bm))
				{
					(void) Spi()->DATA;
					discard--;
				}

				if (flags & SPI_DREIF_bm)
					Spi()->DATA = data_out[i_out++];
			}

			while (i_out < length)
			{
				uint8_t flags = Spi()->INTFLAGS;

				if (flags & (SPI_RXCIF_bm | SPI_BUFOVF_bm | SPI_TXCIF_bm))
				{
					uint8_t data = Spi()->DATA;
					data_in[i_in++] = data;
				}

				if (flags & SPI_DREIF_bm)
					Spi()->DATA = data_out[i_out++];
			}

			while (i_in < length)
			{
				uint8_t flags = Spi()->INTFLAGS;

				if (flags & (SPI_RXCIF_bm | SPI_BUFOVF_bm | SPI_TXCIF_bm))
				{
					uint8_t data = Spi()->DATA;
					if (discard)
						discard--;
					else
						data_in[i_in++] = data;
				}
			}
		}
	}

	virtual void transfer_block(
		uint8_t const * data_out,
		uint8_t       * data_in,
		size_t          length
	) override
	{
		while (length > 255)
		{
			transfer_block_u8(data_out, data_in, 255);
			data_out += 255;
			length -= 255;
			if (data_in)
				data_in += 255;
		}
		if (length)
			transfer_block_u8(data_out, data_in, length);
	}

	void init(clockdiv fsck, bool msb_first, uint8_t mode)
	{
		Spi()->CTRLB = SPI_BUFEN_bm | SPI_BUFWR_bm | SPI_SSD_bm
			| (mode & 0x3);

		Spi()->INTCTRL = 0;

		Spi()->CTRLA = 0
			| (msb_first ? 0 : SPI_DORD_bm)
			| SPI_MASTER_bm
			| (uint8_t) fsck
			| SPI_ENABLE_bm
			;

		// Pump the FIFO so the discards in the transfer loop match up
		transfer_block_u8((uint8_t const *) "\0", NULL, 3);
	}

	SPI_t * Spi() const {
		return
#ifdef SPI0
			NSpi == 0 ? &SPI0 :
#endif
#ifdef SPI1
			NSpi == 1 ? &SPI1 :
#endif
			NULL;
	};
};

#endif // !defined(SPI_AVRDX_HPP)
