// SPDX-License-Identifier: GPL-2.0
/*
 * Copyright (c) 2025, SaluteDevices. All Rights Reserved.
 *
 * Author: Martin Kurbanov <mmkurbanov@salutedevices.com>
 */

#include <linux/mtd/mtd.h>
#include <linux/mtd/spinand.h>
#ifdef __UBOOT__
#include <spi.h>
#include <dm/device_compat.h>
#endif

/**
 * spinand_otp_page_size() - Get SPI-NAND OTP page size
 * @spinand: the spinand device
 *
 * Return: the OTP page size.
 */
size_t spinand_otp_page_size(struct spinand_device *spinand)
{
	struct nand_device *nand = spinand_to_nand(spinand);

	return nanddev_page_size(nand) + nanddev_per_page_oobsize(nand);
}

static size_t spinand_otp_size(struct spinand_device *spinand,
			       const struct spinand_otp_layout *layout)
{
	return layout->npages * spinand_otp_page_size(spinand);
}

/**
 * spinand_fact_otp_size() - Get SPI-NAND factory OTP area size
 * @spinand: the spinand device
 *
 * Return: the OTP size.
 */
size_t spinand_fact_otp_size(struct spinand_device *spinand)
{
	return spinand_otp_size(spinand, &spinand->fact_otp->layout);
}

/**
 * spinand_user_otp_size() - Get SPI-NAND user OTP area size
 * @spinand: the spinand device
 *
 * Return: the OTP size.
 */
size_t spinand_user_otp_size(struct spinand_device *spinand)
{
	return spinand_otp_size(spinand, &spinand->user_otp->layout);
}

static int spinand_otp_check_bounds(struct spinand_device *spinand, loff_t ofs,
				    size_t len,
				    const struct spinand_otp_layout *layout)
{
	if (ofs < 0 || ofs + len > spinand_otp_size(spinand, layout))
		return -EINVAL;

	return 0;
}

static int spinand_user_otp_check_bounds(struct spinand_device *spinand,
					 loff_t ofs, size_t len)
{
	return spinand_otp_check_bounds(spinand, ofs, len,
					&spinand->user_otp->layout);
}

static int spinand_otp_rw(struct spinand_device *spinand, loff_t ofs,
			  size_t len, size_t *retlen, u8 *buf, bool is_write,
			  const struct spinand_otp_layout *layout)
{
	struct nand_page_io_req req = {};
	unsigned long long page;
	size_t copied = 0;
	size_t otp_pagesize = spinand_otp_page_size(spinand);
	int ret;

	if (!len)
		return 0;

	ret = spinand_otp_check_bounds(spinand, ofs, len, layout);
	if (ret)
		return ret;

	ret = spinand_upd_cfg(spinand, CFG_OTP_ENABLE, CFG_OTP_ENABLE);
	if (ret)
		return ret;

	page = ofs;
	req.dataoffs = do_div(page, otp_pagesize);
	req.pos.page = page + layout->start_page;
	req.type = is_write ? NAND_PAGE_WRITE : NAND_PAGE_READ;
	req.mode = MTD_OPS_RAW;
	req.databuf.in = buf;

	while (copied < len) {
		req.datalen = min_t(unsigned int,
				    otp_pagesize - req.dataoffs,
				    len - copied);

		if (is_write)
			ret = spinand_write_page(spinand, &req);
		else
			ret = spinand_read_page(spinand, &req);

		if (ret < 0)
			break;

		req.databuf.in += req.datalen;
		req.pos.page++;
		req.dataoffs = 0;
		copied += req.datalen;
	}

	*retlen = copied;

	if (spinand_upd_cfg(spinand, CFG_OTP_ENABLE, 0)) {
		dev_warn(spinand->slave->dev,
			 "Can not disable OTP mode\n");
		ret = -EIO;
	}

	return ret;
}

/**
 * spinand_fact_otp_read() - Read from OTP area
 * @spinand: the spinand device
 * @ofs: the offset to read
 * @len: the number of data bytes to read
 * @retlen: the pointer to variable to store the number of read bytes
 * @buf: the buffer to store the read data
 *
 * Return: 0 on success, an error code otherwise.
 */
int spinand_fact_otp_read(struct spinand_device *spinand, loff_t ofs,
			  size_t len, size_t *retlen, u8 *buf)
{
	return spinand_otp_rw(spinand, ofs, len, retlen, buf, false,
			      &spinand->fact_otp->layout);
}

/**
 * spinand_user_otp_read() - Read from OTP area
 * @spinand: the spinand device
 * @ofs: the offset to read
 * @len: the number of data bytes to read
 * @retlen: the pointer to variable to store the number of read bytes
 * @buf: the buffer to store the read data
 *
 * Return: 0 on success, an error code otherwise.
 */
int spinand_user_otp_read(struct spinand_device *spinand, loff_t ofs,
			  size_t len, size_t *retlen, u8 *buf)
{
	return spinand_otp_rw(spinand, ofs, len, retlen, buf, false,
			      &spinand->user_otp->layout);
}

/**
 * spinand_user_otp_write() - Write to OTP area
 * @spinand:  the spinand device
 * @ofs: the offset to write to
 * @len: the number of bytes to write
 * @retlen: the pointer to variable to store the number of written bytes
 * @buf: the buffer with data to write
 *
 * Return: 0 on success, an error code otherwise.
 */
int spinand_user_otp_write(struct spinand_device *spinand, loff_t ofs,
			   size_t len, size_t *retlen, const u8 *buf)
{
	return spinand_otp_rw(spinand, ofs, len, retlen, (u8 *)buf, true,
			      &spinand->user_otp->layout);
}

static int spinand_mtd_otp_info(struct mtd_info *mtd, size_t len,
				size_t *retlen, struct otp_info *buf,
				bool is_fact)
{
	struct spinand_device *spinand = mtd_to_spinand(mtd);
	int ret;

	*retlen = 0;

	mutex_lock(&spinand->lock);

	if (is_fact)
		ret = spinand->fact_otp->ops->info(spinand, len, buf, retlen);
	else
		ret = spinand->user_otp->ops->info(spinand, len, buf, retlen);

	mutex_unlock(&spinand->lock);

	return ret;
}

static int spinand_mtd_fact_otp_info(struct mtd_info *mtd, size_t len,
				     size_t *retlen, struct otp_info *buf)
{
	return spinand_mtd_otp_info(mtd, len, retlen, buf, true);
}

static int spinand_mtd_user_otp_info(struct mtd_info *mtd, size_t len,
				     size_t *retlen, struct otp_info *buf)
{
	return spinand_mtd_otp_info(mtd, len, retlen, buf, false);
}

static int spinand_mtd_otp_read(struct mtd_info *mtd, loff_t ofs, size_t len,
				size_t *retlen, u8 *buf, bool is_fact)
{
	struct spinand_device *spinand = mtd_to_spinand(mtd);
	int ret;

	*retlen = 0;

	if (!len)
		return 0;

	ret = spinand_otp_check_bounds(spinand, ofs, len,
				       is_fact ? &spinand->fact_otp->layout :
						 &spinand->user_otp->layout);
	if (ret)
		return ret;

	mutex_lock(&spinand->lock);

	if (is_fact)
		ret = spinand->fact_otp->ops->read(spinand, ofs, len, retlen,
						   buf);
	else
		ret = spinand->user_otp->ops->read(spinand, ofs, len, retlen,
						   buf);

	mutex_unlock(&spinand->lock);

	return ret;
}

static int spinand_mtd_fact_otp_read(struct mtd_info *mtd, loff_t ofs,
				     size_t len, size_t *retlen, u8 *buf)
{
	return spinand_mtd_otp_read(mtd, ofs, len, retlen, buf, true);
}

static int spinand_mtd_user_otp_read(struct mtd_info *mtd, loff_t ofs,
				     size_t len, size_t *retlen, u8 *buf)
{
	return spinand_mtd_otp_read(mtd, ofs, len, retlen, buf, false);
}

static int spinand_mtd_user_otp_write(struct mtd_info *mtd, loff_t ofs,
				      size_t len, size_t *retlen, u_char *buf)
{
	struct spinand_device *spinand = mtd_to_spinand(mtd);
	const struct spinand_user_otp_ops *ops = spinand->user_otp->ops;
	int ret;

	*retlen = 0;

	if (!len)
		return 0;

	ret = spinand_user_otp_check_bounds(spinand, ofs, len);
	if (ret)
		return ret;

	mutex_lock(&spinand->lock);
	ret = ops->write(spinand, ofs, len, retlen, buf);
	mutex_unlock(&spinand->lock);

	return ret;
}

#ifndef __UBOOT__
static int spinand_mtd_user_otp_erase(struct mtd_info *mtd, loff_t ofs,
				      size_t len)
{
	struct spinand_device *spinand = mtd_to_spinand(mtd);
	const struct spinand_user_otp_ops *ops = spinand->user_otp->ops;
	int ret;

	if (!len)
		return 0;

	ret = spinand_user_otp_check_bounds(spinand, ofs, len);
	if (ret)
		return ret;

	mutex_lock(&spinand->lock);
	ret = ops->erase(spinand, ofs, len);
	mutex_unlock(&spinand->lock);

	return ret;
}
#endif

static int spinand_mtd_user_otp_lock(struct mtd_info *mtd, loff_t ofs,
				     size_t len)
{
	struct spinand_device *spinand = mtd_to_spinand(mtd);
	const struct spinand_user_otp_ops *ops = spinand->user_otp->ops;
	int ret;

	if (!len)
		return 0;

	ret = spinand_user_otp_check_bounds(spinand, ofs, len);
	if (ret)
		return ret;

	mutex_lock(&spinand->lock);
	ret = ops->lock(spinand, ofs, len);
	mutex_unlock(&spinand->lock);

	return ret;
}

/**
 * spinand_set_mtd_otp_ops() - Setup OTP methods
 * @spinand: the spinand device
 *
 * Setup OTP methods.
 *
 * Return: 0 on success, a negative error code otherwise.
 */
int spinand_set_mtd_otp_ops(struct spinand_device *spinand)
{
	struct mtd_info *mtd = spinand_to_mtd(spinand);
	const struct spinand_fact_otp_ops *fact_ops = spinand->fact_otp->ops;
	const struct spinand_user_otp_ops *user_ops = spinand->user_otp->ops;

	if (!user_ops && !fact_ops)
		return -EINVAL;

	if (user_ops) {
		if (user_ops->info)
			mtd->_get_user_prot_info = spinand_mtd_user_otp_info;

		if (user_ops->read)
			mtd->_read_user_prot_reg = spinand_mtd_user_otp_read;

		if (user_ops->write)
			mtd->_write_user_prot_reg = spinand_mtd_user_otp_write;

		if (user_ops->lock)
			mtd->_lock_user_prot_reg = spinand_mtd_user_otp_lock;
#ifndef __UBOOT__
		if (user_ops->erase)
			mtd->_erase_user_prot_reg = spinand_mtd_user_otp_erase;
#endif
	}

	if (fact_ops) {
		if (fact_ops->info)
			mtd->_get_fact_prot_info = spinand_mtd_fact_otp_info;

		if (fact_ops->read)
			mtd->_read_fact_prot_reg = spinand_mtd_fact_otp_read;
	}

	return 0;
}
