/*
 * Copyright (c) 2019-2022, STMicroelectronics - All Rights Reserved
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

#include <assert.h>
#include <errno.h>
#include <stddef.h>

#include <common/debug.h>
#include <drivers/delay_timer.h>
#include <drivers/nand.h>
#include <lib/utils.h>

#include <platform_def.h>

/*
 * Define a single nand_device used by specific NAND frameworks.
 */
static struct nand_device nand_dev;

#pragma weak plat_get_scratch_buffer
void plat_get_scratch_buffer(void **buffer_addr, size_t *buf_size)
{
	static uint8_t scratch_buff[PLATFORM_MTD_MAX_PAGE_SIZE];

	assert(buffer_addr != NULL);
	assert(buf_size != NULL);

	*buffer_addr = (void *)scratch_buff;
	*buf_size = sizeof(scratch_buff);
}

int nand_read(unsigned int offset, uintptr_t buffer, size_t length,
	      size_t *length_read)
{
	unsigned int block = offset / nand_dev.block_size;
	unsigned int end_block = (offset + length - 1U) / nand_dev.block_size;
	unsigned int page_start =
		(offset % nand_dev.block_size) / nand_dev.page_size;
	unsigned int nb_pages = nand_dev.block_size / nand_dev.page_size;
	unsigned int start_offset = offset % nand_dev.page_size;
	unsigned int page;
	unsigned int bytes_read;
	int is_bad;
	int ret;
	uint8_t *scratch_buff;
	size_t scratch_buff_size;

	plat_get_scratch_buffer((void **)&scratch_buff, &scratch_buff_size);

	assert(scratch_buff != NULL);

	VERBOSE("Block %u - %u, page_start %u, nb %u, length %zu, offset %u\n",
		block, end_block, page_start, nb_pages, length, offset);

	*length_read = 0UL;

	if (((start_offset != 0U) || (length % nand_dev.page_size) != 0U) &&
	    (scratch_buff_size < nand_dev.page_size)) {
		return -EINVAL;
	}

	while (block <= end_block) {
		is_bad = nand_dev.mtd_block_is_bad(block);
		if (is_bad < 0) {
			return is_bad;
		}

		if (is_bad == 1) {
			/* Skip the block */
			uint32_t max_block =
				nand_dev.size / nand_dev.block_size;

			block++;
			end_block++;
			if ((block < max_block) && (end_block < max_block)) {
				continue;
			}

			return -EIO;
		}

		for (page = page_start; page < nb_pages; page++) {
			if ((start_offset != 0U) ||
			    (length < nand_dev.page_size)) {
				ret = nand_dev.mtd_read_page(
						&nand_dev,
						(block * nb_pages) + page,
						(uintptr_t)scratch_buff);
				if (ret != 0) {
					return ret;
				}

				bytes_read = MIN((size_t)(nand_dev.page_size -
							  start_offset),
						 length);

				memcpy((uint8_t *)buffer,
				       scratch_buff + start_offset,
				       bytes_read);

				start_offset = 0U;
			} else {
				ret = nand_dev.mtd_read_page(&nand_dev,
						(block * nb_pages) + page,
						buffer);
				if (ret != 0) {
					return ret;
				}

				bytes_read = nand_dev.page_size;
			}

			length -= bytes_read;
			buffer += bytes_read;
			*length_read += bytes_read;

			if (length == 0U) {
				break;
			}
		}

		page_start = 0U;
		block++;
	}

	return 0;
}

int nand_seek_bb(uintptr_t base, unsigned int offset, size_t *extra_offset)
{
	unsigned int block;
	unsigned int offset_block;
	unsigned int max_block;
	int is_bad;
	size_t count_bb = 0U;

	block = base / nand_dev.block_size;

	if (offset != 0U) {
		offset_block = (base + offset - 1U) / nand_dev.block_size;
	} else {
		offset_block = block;
	}

	max_block = nand_dev.size / nand_dev.block_size;

	while (block <= offset_block) {
		if (offset_block >= max_block) {
			return -EIO;
		}

		is_bad = nand_dev.mtd_block_is_bad(block);
		if (is_bad < 0) {
			return is_bad;
		}

		if (is_bad == 1) {
			count_bb++;
			offset_block++;
		}

		block++;
	}

	*extra_offset = count_bb * nand_dev.block_size;

	return 0;
}

struct nand_device *get_nand_device(void)
{
	return &nand_dev;
}
