// SPDX-License-Identifier: GPL-2.0
/*
 * Copyright (c) 2019 MediaTek Inc.
 */

#include <linux/soc/mediatek/mtk-cmdq-ext.h>

#include "cmdq-util.h"
#include "cmdq-sec.h"
#include "cmdq-sec-mailbox.h"

#define ADDR_METADATA_MAX_COUNT_ORIGIN	(8)

#define CMDQ_IMMEDIATE_VALUE		(0)
#define CMDQ_REG_TYPE			(1)

static s32 cmdq_sec_realloc_addr_list(struct cmdq_pkt *pkt, const u32 count)
{
	struct cmdq_sec_data *sec_data =
		(struct cmdq_sec_data *)pkt->sec_data;
	void *prev = (void *)(unsigned long)sec_data->addrMetadatas, *curr;

	if (count <= sec_data->addrMetadataMaxCount)
		return 0;

	curr = kcalloc(count, sizeof(*sec_data), GFP_KERNEL);
	if (!curr)
		return -ENOMEM;
	if (count && sec_data->addrMetadatas)
		memcpy(curr, prev,
			sizeof(*sec_data) * sec_data->addrMetadataMaxCount);
	kfree(prev);

	sec_data->addrMetadatas = (u64)curr;
	sec_data->addrMetadataMaxCount = count;
	return 0;
}

static s32 cmdq_sec_check_sec(struct cmdq_pkt *pkt)
{
	struct cmdq_sec_data *sec_data;

	if (pkt->sec_data)
		return 0;

	sec_data = kzalloc(sizeof(*sec_data), GFP_KERNEL);
	if (!sec_data)
		return -ENOMEM;
	pkt->sec_data = (void *)sec_data;

	return 0;
}

static s32 cmdq_sec_append_metadata(
	struct cmdq_pkt *pkt, const enum CMDQ_IWC_ADDR_METADATA_TYPE type,
	const u64 base, const u32 offset, const u32 size,
	const u32 port, struct cmdq_reuse *reuse)
{
	struct cmdq_sec_data *sec_data;
	struct cmdq_sec_addr_meta *meta;
	s32 idx, max, ret;

	cmdq_log("pkt:%p type:%u base:%#llx offset:%#x size:%#x port:%#x",
		pkt, type, base, offset, size, port);

	ret = cmdq_sec_check_sec(pkt);
	if (ret < 0)
		return ret;
	sec_data = (struct cmdq_sec_data *)pkt->sec_data;
	idx = sec_data->addrMetadataCount;

	if (idx >= CMDQ_IWC_MAX_ADDR_LIST_LENGTH) {
		cmdq_err("idx:%u reach over:%u",
			idx, CMDQ_IWC_MAX_ADDR_LIST_LENGTH);
		return -EFAULT;
	}

	if (!sec_data->addrMetadataMaxCount)
		max = ADDR_METADATA_MAX_COUNT_ORIGIN;
	else if (idx >= sec_data->addrMetadataMaxCount)
		max = sec_data->addrMetadataMaxCount * 2;
	else
		max = sec_data->addrMetadataMaxCount;
	ret = cmdq_sec_realloc_addr_list(pkt, max);
	if (ret)
		return ret;

	if (!sec_data->addrMetadatas) {
		cmdq_log("addrMetadatas is missing");
		meta = kzalloc((sizeof(*meta) * max), GFP_KERNEL);
		if (!meta)
			return -ENOMEM;
		sec_data->addrMetadatas = (u64)(void *)meta;
	}
	meta = (struct cmdq_sec_addr_meta *)
		(unsigned long)sec_data->addrMetadatas;

	if (reuse)
		reuse->meta_idx = idx;

	if (idx < max) {
		meta[idx].instrIndex = pkt->cmd_buf_size / CMDQ_INST_SIZE - 1;
		meta[idx].type = type;
		meta[idx].baseHandle = base;
		meta[idx].offset = offset;
		meta[idx].size = size;
		meta[idx].port = port;
		sec_data->addrMetadataCount += 1;
	} else {
		cmdq_err("meta + idx:%d make out of bounds write max:%d",
			idx, max);
		return -ENOMEM;
	}
	return 0;
}

s32 cmdq_sec_pkt_set_data(struct cmdq_pkt *pkt, const u64 dapc_engine,
	const u64 port_sec_engine, const enum CMDQ_SEC_SCENARIO scenario,
	const enum cmdq_sec_meta_type meta_type)
{
	struct cmdq_sec_data *sec_data;
	s32 ret;

	if (!pkt) {
		cmdq_err("invalid pkt:%p", pkt);
		return -EINVAL;
	}

	ret = cmdq_sec_check_sec(pkt);
	if (ret < 0)
		return ret;
	cmdq_log(
		"pkt:%p sec_data:%p dapc:%llu port_sec:%llu scen:%u",
		pkt, pkt->sec_data, dapc_engine, port_sec_engine, scenario);

	sec_data = (struct cmdq_sec_data *)pkt->sec_data;
	sec_data->enginesNeedDAPC |= dapc_engine;
	sec_data->enginesNeedPortSecurity |= port_sec_engine;
	sec_data->scenario = scenario;
	sec_data->client_meta_type = meta_type;

	return 0;
}
EXPORT_SYMBOL(cmdq_sec_pkt_set_data);

void cmdq_sec_pkt_set_mtee(struct cmdq_pkt *pkt, const bool enable)
{
	struct cmdq_sec_data *sec_data =
		(struct cmdq_sec_data *)pkt->sec_data;
	sec_data->mtee = enable;
	sec_data->sec_id = 0;
	cmdq_log("%s pkt:%p mtee:%d sec_id:%d\n",
		__func__, pkt, ((struct cmdq_sec_data *)pkt->sec_data)->mtee,
		((struct cmdq_sec_data *)pkt->sec_data)->sec_id);
}
EXPORT_SYMBOL(cmdq_sec_pkt_set_mtee);

void cmdq_sec_pkt_free_data(struct cmdq_pkt *pkt)
{
	if (pkt->sec_data == NULL)
		return;
	kfree((void *)((struct cmdq_sec_data *)pkt->sec_data)->addrMetadatas);
	kfree(pkt->sec_data);
}
EXPORT_SYMBOL(cmdq_sec_pkt_free_data);

s32 cmdq_sec_pkt_set_payload(struct cmdq_pkt *pkt, u8 idx,
	const u32 meta_size, u32 *meta)
{
	struct cmdq_sec_data *sec_data;
	s32 ret;

	if (idx == 0) {
		cmdq_err("not allow set reserved payload 0");
		return -EINVAL;
	}

	if (!meta_size || !meta) {
		cmdq_err("not allow empty size or buffer");
		return -EINVAL;
	}

	ret = cmdq_sec_check_sec(pkt);
	if (ret < 0)
		return ret;

	if (idx == CMDQ_IWC_MSG1 &&
		meta_size >= sizeof(struct iwcCmdqMessageEx_t)) {
		cmdq_err("not enough size payload 1:%u msg size:%zu",
			meta_size, sizeof(struct iwcCmdqMessageEx_t));
		return -EINVAL;
	}

	if (idx == CMDQ_IWC_MSG2 &&
		meta_size >= sizeof(struct iwcCmdqMessageEx2_t)) {
		cmdq_err("not enough size payload 2:%u msg size:%zu",
			meta_size, sizeof(struct iwcCmdqMessageEx2_t));
		return -EINVAL;
	}

	sec_data = (struct cmdq_sec_data *)pkt->sec_data;
	sec_data->client_meta_size[idx] = meta_size;
	sec_data->client_meta[idx] = meta;

	return 0;
}
EXPORT_SYMBOL(cmdq_sec_pkt_set_payload);

s32 cmdq_sec_pkt_write_reg(struct cmdq_pkt *pkt, u32 addr, u64 base,
	const enum CMDQ_IWC_ADDR_METADATA_TYPE type,
	const u32 offset, const u32 size, const u32 port)
{
	s32 ret;

	ret = cmdq_pkt_assign_command(pkt, CMDQ_SPR_FOR_TEMP, addr);
	if (ret)
		return ret;

	ret = cmdq_pkt_append_command(pkt,
		base & 0xffff, base >> 16, CMDQ_SPR_FOR_TEMP, 0,
		CMDQ_IMMEDIATE_VALUE, CMDQ_IMMEDIATE_VALUE, CMDQ_REG_TYPE,
		CMDQ_CODE_WRITE_S);
	if (ret)
		return ret;

	return cmdq_sec_append_metadata(pkt, type, base, offset, size, port, NULL);
}
EXPORT_SYMBOL(cmdq_sec_pkt_write_reg);

s32 cmdq_sec_pkt_write_reg_reuse(struct cmdq_pkt *pkt, u32 addr, u64 base,
	const enum CMDQ_IWC_ADDR_METADATA_TYPE type,
	const u32 offset, const u32 size, const u32 port, struct cmdq_reuse *reuse)
{
	s32 ret;

	ret = cmdq_pkt_assign_command(pkt, CMDQ_SPR_FOR_TEMP, addr);
	if (ret)
		return ret;

	ret = cmdq_pkt_append_command(pkt,
		base & 0xffff, base >> 16, CMDQ_SPR_FOR_TEMP, 0,
		CMDQ_IMMEDIATE_VALUE, CMDQ_IMMEDIATE_VALUE, CMDQ_REG_TYPE,
		CMDQ_CODE_WRITE_S);
	if (ret)
		return ret;

	reuse->op = CMDQ_CODE_WRITE_S;
	reuse->va = cmdq_pkt_get_curr_buf_va(pkt) - CMDQ_INST_SIZE;
	reuse->offset = pkt->cmd_buf_size - CMDQ_INST_SIZE;
	reuse->sec = true;

	return cmdq_sec_append_metadata(pkt, type, base, offset, size, port, reuse);
}
EXPORT_SYMBOL(cmdq_sec_pkt_write_reg_reuse);

s32 cmdq_sec_pkt_assign_metadata(struct cmdq_pkt *pkt,
	u32 count, void *meta_array)
{
	struct cmdq_sec_data *data;
	void *pkt_meta_array;
	size_t size;
	s32 ret;

	if (!count)
		return -EINVAL;

	ret = cmdq_sec_check_sec(pkt);
	if (ret < 0)
		return ret;
	data = (struct cmdq_sec_data *)pkt->sec_data;

	size = count * sizeof(struct cmdq_sec_addr_meta);
	pkt_meta_array = kzalloc(size, GFP_KERNEL);
	if (!pkt_meta_array)
		return -ENOMEM;

	memcpy(pkt_meta_array, meta_array, size);
	if ((void *)data->addrMetadatas)
		kfree((void *)data->addrMetadatas);
	data->addrMetadatas = (unsigned long)pkt_meta_array;
	data->addrMetadataCount = count;

	return 0;
}
EXPORT_SYMBOL(cmdq_sec_pkt_assign_metadata);

void cmdq_sec_dump_secure_data(struct cmdq_pkt *pkt)
{
	struct cmdq_sec_data *data;
	struct cmdq_sec_addr_meta *meta;
	s32 i;
	u64 *inst;

	if (!pkt || !pkt->sec_data) {
		cmdq_msg("pkt without sec_data");
		return;
	}

	data = (struct cmdq_sec_data *)pkt->sec_data;
	cmdq_util_msg(
		"meta cnt:%u addr:%#llx max:%u scen:%d dapc:%#llx port:%#llx wait:%d reset:%d metatype:%u",
		data->addrMetadataCount, data->addrMetadatas,
		data->addrMetadataMaxCount, data->scenario,
		data->enginesNeedDAPC, data->enginesNeedPortSecurity,
		data->waitCookie, data->resetExecCnt,
		(u32)data->client_meta_type);

	meta = (struct cmdq_sec_addr_meta *)(unsigned long)
		data->addrMetadatas;
	for (i = 0; i < data->addrMetadataCount; i++) {
		inst = cmdq_pkt_get_va_by_offset(pkt,
			meta[i].instrIndex * CMDQ_INST_SIZE);
		cmdq_util_msg(
			"meta:%d instr:%u type:%u base:%#llx block:%u ofst:%u size:%u port:%u inst:%#018llx",
			i, meta[i].instrIndex, meta[i].type,
			meta[i].baseHandle, meta[i].blockOffset, meta[i].offset,
			meta[i].size, meta[i].port,
			inst ? *inst : 0);
	}
}
EXPORT_SYMBOL(cmdq_sec_dump_secure_data);

int cmdq_sec_pkt_wait_complete(struct cmdq_pkt *pkt)
{
	struct cmdq_client *client = pkt->cl;
	unsigned long ret;
	u8 cnt = 0;
	s32 thread_id = cmdq_sec_mbox_chan_id(client->chan);
	u32 timeout_ms = cmdq_mbox_get_thread_timeout((void *)client->chan);

#if IS_ENABLED(CONFIG_MMPROFILE)
	cmdq_sec_mmp_wait(client->chan, pkt);
#endif

	cmdq_sec_mbox_enable(client->chan);

	do {
		if (timeout_ms == CMDQ_NO_TIMEOUT) {
			cmdq_msg("%s: timeout:%u", __func__, timeout_ms);
			wait_for_completion(&pkt->cmplt);
			break;
		}

		ret = wait_for_completion_timeout(&pkt->cmplt,
			msecs_to_jiffies(CMDQ_PREDUMP_MS(timeout_ms)));
		if (ret)
			break;

		cmdq_util_dump_lock();

		cmdq_msg("===== SW timeout Pre-dump %hhu =====", cnt);
		cnt++;

		cmdq_dump_core(client->chan);
		cmdq_msg("thd:%d Hidden thread info since it's secure",
			thread_id);
		cmdq_sec_dump_operation(client->chan);
		cmdq_sec_dump_secure_thread_cookie(client->chan);
		cmdq_dump_pkt(pkt, 0, false);
		cmdq_sec_dump_notify_loop(client->chan);

		cmdq_util_dump_unlock();
	} while (1);

	cmdq_sec_mbox_disable(client->chan);

#if IS_ENABLED(CONFIG_MMPROFILE)
	cmdq_sec_mmp_wait_done(client->chan, pkt);
#endif

	return 0;
}
EXPORT_SYMBOL(cmdq_sec_pkt_wait_complete);

void cmdq_sec_err_dump(struct cmdq_pkt *pkt, struct cmdq_client *client,
	u64 **inst, const char **dispatch)
{
	cmdq_sec_dump_operation(client->chan);
	cmdq_sec_dump_secure_thread_cookie(client->chan);
	cmdq_sec_dump_notify_loop(client->chan);
	cmdq_sec_dump_secure_data(pkt);
	cmdq_sec_dump_response(client->chan, pkt, inst, dispatch);
}
EXPORT_SYMBOL(cmdq_sec_err_dump);

MODULE_LICENSE("GPL v2");
