/* SPDX-License-Identifier: GPL-2.0-only */

#include <spi-generic.h>
#include <spi_flash.h>
#include <arch/cache.h>
#include <device/mmio.h>
#include <soc/addressmap.h>
#include <soc/qspi.h>
#include <soc/gpio.h>
#include <soc/clock.h>
#include <symbols.h>
#include <assert.h>
#include <gpio.h>
#include <string.h>

#define CACHE_LINE_SIZE	64

static int curr_desc_idx = -1;

struct cmd_desc {
	uint32_t data_address;
	uint32_t next_descriptor;
	uint32_t direction:1;
	uint32_t multi_io_mode:3;
	uint32_t reserved1:4;
	uint32_t fragment:1;
	uint32_t reserved2:7;
	uint32_t length:16;
	//------------------------//
	uint32_t bounce_src;
	uint32_t bounce_dst;
	uint32_t bounce_length;
	uint64_t padding[5];
};

enum qspi_mode {
	SDR_1BIT = 1,
	SDR_2BIT = 2,
	SDR_4BIT = 3,
	DDR_1BIT = 5,
	DDR_2BIT = 6,
	DDR_4BIT = 7,
};

enum cs_state {
	CS_DEASSERT,
	CS_ASSERT
};

struct xfer_cfg {
	enum qspi_mode mode;
};

enum bus_xfer_direction {
	MASTER_READ = 0,
	MASTER_WRITE = 1,
};

struct {
	struct cmd_desc descriptors[3];
	uint8_t buffers[3][CACHE_LINE_SIZE];
} *dma = (void *)_dma_coherent;

static void dma_transfer_chain(struct cmd_desc *chain)
{
	uint32_t mstr_int_status;

	write32(&sdm845_qspi->mstr_int_sts, 0xFFFFFFFF);
	write32(&sdm845_qspi->next_dma_desc_addr, (uint32_t)(uintptr_t) chain);

	while (1) {
		mstr_int_status = read32(&sdm845_qspi->mstr_int_sts);
		if (mstr_int_status & DMA_CHAIN_DONE)
			break;
	}
}

static void flush_chain(void)
{
	struct cmd_desc *desc = &dma->descriptors[0];
	uint8_t *src;
	uint8_t *dst;

	dma_transfer_chain(desc);

	while (desc) {
		if (desc->direction == MASTER_READ) {
			if (desc->bounce_length == 0)
				dcache_invalidate_by_mva(
					(void *)(uintptr_t) desc->data_address,
					desc->length);
			else {
				src = (void *)(uintptr_t) desc->bounce_src;
				dst = (void *)(uintptr_t) desc->bounce_dst;
				memcpy(dst, src, desc->bounce_length);
			}
		}
		desc = (void *)(uintptr_t) desc->next_descriptor;
	}
	curr_desc_idx = -1;
}

static struct cmd_desc *allocate_descriptor(void)
{
	struct cmd_desc *current;
	struct cmd_desc *next;
	uint8_t index;

	current = (curr_desc_idx == -1) ?
		NULL : &dma->descriptors[curr_desc_idx];

	index = ++curr_desc_idx;
	next = &dma->descriptors[index];

	next->data_address = (uint32_t) (uintptr_t) dma->buffers[index];

	next->next_descriptor = 0;
	next->direction = MASTER_READ;
	next->multi_io_mode = 0;
	next->reserved1 = 0;
	next->fragment = 0;
	next->reserved2 = 0;
	next->length = 0;
	next->bounce_src = 0;
	next->bounce_dst = 0;
	next->bounce_length = 0;

	if (current) {
		current->next_descriptor = (uint32_t)(uintptr_t) next;
		current->fragment = 1;
	}

	return next;
}

static void cs_change(enum cs_state state)
{
	gpio_set(GPIO(90), state == CS_DEASSERT);
}

static void configure_gpios(void)
{
	gpio_output(GPIO(90), 1);

	gpio_configure(GPIO(91), GPIO91_FUNC_QSPI_DATA,
		GPIO_NO_PULL, GPIO_2MA, GPIO_ENABLE);

	gpio_configure(GPIO(92), GPIO92_FUNC_QSPI_DATA,
		GPIO_NO_PULL, GPIO_2MA, GPIO_ENABLE);

	gpio_configure(GPIO(95), GPIO95_FUNC_QSPI_CLK,
		GPIO_NO_PULL, GPIO_2MA, GPIO_ENABLE);
}

static void queue_bounce_data(uint8_t *data, uint32_t data_bytes,
			      enum qspi_mode data_mode, bool write)
{
	struct cmd_desc *desc;
	uint8_t *ptr;

	desc = allocate_descriptor();
	desc->direction = write;
	desc->multi_io_mode = data_mode;
	ptr = (void *)(uintptr_t) desc->data_address;

	if (write) {
		memcpy(ptr, data, data_bytes);
	} else {
		desc->bounce_src = (uint32_t)(uintptr_t) ptr;
		desc->bounce_dst = (uint32_t)(uintptr_t) data;
		desc->bounce_length = data_bytes;
	}

	desc->length = data_bytes;
}

static void queue_direct_data(uint8_t *data, uint32_t data_bytes,
			      enum qspi_mode data_mode, bool write)
{
	struct cmd_desc *desc;

	desc = allocate_descriptor();
	desc->direction = write;
	desc->multi_io_mode = data_mode;
	desc->data_address = (uint32_t)(uintptr_t) data;
	desc->length = data_bytes;

	if (write)
		dcache_clean_by_mva(data, data_bytes);
	else
		dcache_invalidate_by_mva(data, data_bytes);
}

static void queue_data(uint8_t *data, uint32_t data_bytes,
	enum qspi_mode data_mode, bool write)
{
	uint8_t *aligned_ptr;
	uint8_t *epilog_ptr;
	uint32_t prolog_bytes, aligned_bytes, epilog_bytes;

	if (data_bytes == 0)
		return;

	aligned_ptr =
		(uint8_t *)ALIGN_UP((uintptr_t)data, CACHE_LINE_SIZE);

	prolog_bytes = MIN(data_bytes, aligned_ptr - data);
	aligned_bytes = ALIGN_DOWN(data_bytes - prolog_bytes, CACHE_LINE_SIZE);
	epilog_bytes = data_bytes - prolog_bytes - aligned_bytes;

	epilog_ptr = data + prolog_bytes + aligned_bytes;

	if (prolog_bytes)
		queue_bounce_data(data, prolog_bytes, data_mode, write);
	if (aligned_bytes)
		queue_direct_data(aligned_ptr, aligned_bytes, data_mode, write);
	if (epilog_bytes)
		queue_bounce_data(epilog_ptr, epilog_bytes, data_mode, write);
}

static void reg_init(void)
{
	uint32_t spi_mode;
	uint32_t tx_data_oe_delay, tx_data_delay;
	uint32_t mstr_config;

	spi_mode = 0;

	tx_data_oe_delay = 0;
	tx_data_delay = 0;

	mstr_config = (tx_data_oe_delay << TX_DATA_OE_DELAY_SHIFT) |
		(tx_data_delay << TX_DATA_DELAY_SHIFT) | (SBL_EN) |
		(spi_mode << SPI_MODE_SHIFT) |
		(PIN_HOLDN) |
		(FB_CLK_EN) |
		(DMA_ENABLE) |
		(FULL_CYCLE_MODE);

	write32(&sdm845_qspi->mstr_cfg, mstr_config);
	write32(&sdm845_qspi->ahb_mstr_cfg, 0xA42);
	write32(&sdm845_qspi->mstr_int_en, 0x0);
	write32(&sdm845_qspi->mstr_int_sts, 0xFFFFFFFF);
	write32(&sdm845_qspi->rd_fifo_cfg, 0x0);
	write32(&sdm845_qspi->rd_fifo_rst, RESET_FIFO);
}

void quadspi_init(uint32_t hz)
{
	assert(dcache_line_bytes() == CACHE_LINE_SIZE);
	clock_configure_qspi(hz * 4);
	configure_gpios();
	reg_init();
}

int sdm845_claim_bus(const struct spi_slave *slave)
{
	cs_change(CS_ASSERT);
	return 0;
}

void sdm845_release_bus(const struct spi_slave *slave)
{
	cs_change(CS_DEASSERT);
}

static int xfer(enum qspi_mode mode, const void *dout, size_t out_bytes,
		void *din, size_t in_bytes)
{
	if ((out_bytes && !dout) || (in_bytes && !din) ||
		(in_bytes && out_bytes)) {
		return -1;
	}

	queue_data((uint8_t *) (out_bytes ? dout : din),
		in_bytes | out_bytes, mode, !!out_bytes);

	flush_chain();

	return 0;
}

int sdm845_xfer(const struct spi_slave *slave, const void *dout,
		size_t out_bytes, void *din, size_t in_bytes)
{
	return xfer(SDR_1BIT, dout, out_bytes, din, in_bytes);
}

int sdm845_xfer_dual(const struct spi_slave *slave, const void *dout,
		     size_t out_bytes, void *din, size_t in_bytes)
{
	return xfer(SDR_2BIT, dout, out_bytes, din, in_bytes);
}
