// SPDX-License-Identifier: GPL-2.0-only
// SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.

#include <linux/kernel.h> /* printk() */
#include <linux/vmalloc.h>   /* kmalloc() */
#include <linux/fs.h>   /* everything... */
#include <linux/errno.h> /* error codes */
#include <linux/fcntl.h> /* O_ACCMODE */
#include <asm/uaccess.h>
#include <asm-generic/bug.h>
#include <linux/mmc/ioctl.h>
#include <linux/mmc/core.h>
#include "tegra_vblk.h"

#define MMC_SWITCH_MODE_WRITE                         ((uint32_t)0x3)
#define EXT_CSD_MODE_CONFIG                             30
#define EXT_CSD_MODE_CONFIG_FFU                      1
#define MMC_SWITCH_CMDSET_DFLT                        0x01U
#define MMC_SWITCH                      6

int vblk_prep_mmc_multi_ioc(struct vblk_dev *vblkdev,
	struct vblk_ioctl_req *ioctl_req,
	void __user *user,
		uint32_t cmd)
{
	int err = 0;
	struct combo_info_t *combo_info;
	struct combo_cmd_t *combo_cmd;
	int i = 0;
	uint64_t num_cmd;
	struct mmc_ioc_cmd ic;
	struct mmc_ioc_multi_cmd __user *user_cmd;
	struct mmc_ioc_cmd __user *usr_ptr;
	uint32_t combo_cmd_size;
	uint32_t ioctl_bytes = EMMC_IOCTL_MAX_SIZE;
	uint8_t *tmpaddr;
	void *ioctl_buf;

	ioctl_buf = vmalloc(ioctl_bytes);
	if (ioctl_buf == NULL)
		return -ENOMEM;

	combo_info = (struct combo_info_t *)ioctl_buf;
	combo_cmd_size = sizeof(uint32_t);

	if (cmd == MMC_IOC_MULTI_CMD) {
		user_cmd = (struct mmc_ioc_multi_cmd __user *)user;
		if (copy_from_user(&num_cmd, &user_cmd->num_of_cmds,
				sizeof(num_cmd))) {
			err = -EFAULT;
			goto free_ioc_buf;
		}

		if (num_cmd > MMC_IOC_MAX_CMDS) {
			err = -EINVAL;
			goto free_ioc_buf;
		}

		usr_ptr = (void __user *)&user_cmd->cmds;
	} else {
		num_cmd = 1;
		usr_ptr = (void __user *)user;
	}
	combo_info->count = num_cmd;

	combo_cmd = (struct combo_cmd_t *)(ioctl_buf +
		sizeof(struct combo_info_t));

	combo_cmd_size = sizeof(struct combo_info_t) +
		sizeof(struct combo_cmd_t) * combo_info->count;
	if (combo_cmd_size < sizeof(struct combo_info_t)) {
		dev_err(vblkdev->device,
			"combo_cmd_size is overflowing!\n");
		err = -EINVAL;
		goto free_ioc_buf;
	}

	/* should be 512 byte aligned */
	combo_cmd_size = ALIGN(combo_cmd_size, SZ_512);

	if (combo_cmd_size > ioctl_bytes) {
		dev_err(vblkdev->device,
			" buffer has no enough space to serve ioctl %d %d\n",
			combo_cmd_size, ioctl_bytes);
		err = -EFAULT;
		goto free_ioc_buf;
	}

	tmpaddr = (uint8_t *)&ic;
	for (i = 0; i < combo_info->count; i++) {
		if (copy_from_user((void *)tmpaddr, usr_ptr, sizeof(ic))) {
			err = -EFAULT;
			goto free_ioc_buf;
		}
		combo_cmd->cmd = ic.opcode;
		combo_cmd->arg = ic.arg;


		if (combo_cmd->cmd == MMC_SWITCH &&
				(combo_cmd->arg ==
				 (((uint32_t)MMC_SWITCH_MODE_WRITE << 24) |
				 ((uint32_t)EXT_CSD_MODE_CONFIG << 16) |
				 ((uint32_t)EXT_CSD_MODE_CONFIG_FFU << 8) |
				 (uint32_t)MMC_SWITCH_CMDSET_DFLT)) &&
				!(vblkdev->allow_ffu_passthrough_cmds)) {
			dev_err(vblkdev->device,
					"FFU permission not present\n");
			err = -EPERM;
			goto free_ioc_buf;
		}

		combo_cmd->write_flag = (uint32_t)ic.write_flag;
		combo_cmd->data_len = (uint32_t)(ic.blksz * ic.blocks);
		combo_cmd->buf_offset = combo_cmd_size;
		combo_cmd_size += combo_cmd->data_len;
		if ((combo_cmd_size < combo_cmd->data_len) ||
				(combo_cmd_size > ioctl_bytes)) {
			dev_err(vblkdev->device,
				" buffer has no enough space to serve ioctl\n");
			err = -EFAULT;
			goto free_ioc_buf;
		}

		if (ic.write_flag && combo_cmd->data_len) {
			if (copy_from_user((
				(void *)ioctl_buf +
				combo_cmd->buf_offset),
				(void __user *)(unsigned long)ic.data_ptr,
				(u64)combo_cmd->data_len))
			{
				dev_err(vblkdev->device,
					"copy from user failed for data!\n");
				err = -EFAULT;
				goto free_ioc_buf;
			}
		}
		combo_cmd++;
		usr_ptr++;
	}

	ioctl_req->ioctl_id = VBLK_MMC_MULTI_IOC_ID;
	ioctl_req->ioctl_buf = ioctl_buf;
	ioctl_req->ioctl_len = ioctl_bytes;

free_ioc_buf:
	if (err && ioctl_buf)
		vfree(ioctl_buf);

	return err;
}

int vblk_complete_mmc_multi_ioc(struct vblk_dev *vblkdev,
		struct vblk_ioctl_req *ioctl_req,
		void __user *user,
		uint32_t cmd)
{
	uint64_t num_cmd;
	struct mmc_ioc_cmd ic;
	struct mmc_ioc_cmd *ic_ptr = &ic;
	struct mmc_ioc_multi_cmd __user *user_cmd;
	struct mmc_ioc_cmd __user *usr_ptr;
	struct combo_cmd_t *combo_cmd;
	uint32_t i;
	int err = 0;
	void *ioctl_buf = ioctl_req->ioctl_buf;

	if (ioctl_req->status) {
		err = ioctl_req->status;
		if (ioctl_req->ioctl_buf)
			vfree(ioctl_req->ioctl_buf);
		goto exit;
	}

	if (cmd == MMC_IOC_MULTI_CMD) {
		user_cmd = (struct mmc_ioc_multi_cmd __user *)user;
		if (copy_from_user(&num_cmd, &user_cmd->num_of_cmds,
				sizeof(num_cmd))) {
			err = -EFAULT;
			goto free_ioc_buf;
		}

		if (num_cmd > MMC_IOC_MAX_CMDS) {
			err = -EINVAL;
			goto free_ioc_buf;
		}

		usr_ptr = (void __user *)&user_cmd->cmds;
	} else {
		usr_ptr = (void __user *)user;
		num_cmd = 1;
	}

	combo_cmd = (struct combo_cmd_t *)(ioctl_buf +
			sizeof(struct combo_info_t));

	for (i = 0; i < num_cmd; i++) {
		if (copy_from_user((void *)ic_ptr, usr_ptr,
			sizeof(struct mmc_ioc_cmd))) {
			err = -EFAULT;
			goto free_ioc_buf;
		}

		if (copy_to_user(&(usr_ptr->response), combo_cmd->response,
			sizeof(combo_cmd->response))) {
			err = -EFAULT;
			goto free_ioc_buf;
		}

		if (!ic.write_flag && combo_cmd->data_len) {
			if (copy_to_user(
				(void __user *)(unsigned long)ic.data_ptr,
				(ioctl_buf + combo_cmd->buf_offset),
				(u64)combo_cmd->data_len))
			{
				dev_err(vblkdev->device,
					"copy to user of ioctl data failed!\n");
				err = -EFAULT;
				goto free_ioc_buf;
			}
		}
		combo_cmd++;
		usr_ptr++;
	}

free_ioc_buf:
	if (ioctl_buf)
		vfree(ioctl_buf);

exit:
	return err;
}
