// SPDX-License-Identifier: (GPL-2.0 OR BSD-3-Clause)
/*
 * Copyright (c) 2020 MediaTek Inc.
 */

#include <linux/of_device.h>
#include <linux/io.h>
#include <linux/spinlock.h>
#include <linux/mutex.h>
#include <linux/interrupt.h>
#include <linux/completion.h>
#include <linux/slab.h>
#include <linux/mm.h>
#include <linux/debugfs.h>
#include <linux/version.h>
#include <linux/dma-direct.h>
#include <linux/sched.h>
#include <linux/sched/clock.h>

#include <common/mdla_device.h>
#include <common/mdla_cmd_proc.h>
#include <common/mdla_power_ctrl.h>
#include <common/mdla_ioctl.h>
#include <common/mdla_scheduler.h>
#include <utilities/mdla_debug.h>
#include <utilities/mdla_profile.h>
#include <utilities/mdla_util.h>
#include <utilities/mdla_trace.h>
#include <interface/mdla_cmd_data_v2_0.h>
#include <platform/mdla_plat_api.h>

#include "comm_driver.h"
#include "mdla_cmd_data_v2_0.h"
#include "mdla_hw_reg_v2_0.h"
#include "mdla_plat_vlm.h"

#define BACKUP_ADDR1 0x0
#define BACKUP_ADDR2 0x8
#define BACKUP_ADDR3 0x54
#define BACKUP_ADDR4 0xB4

#define MAX_VLM_CTX (3)

static DEFINE_SPINLOCK(ctx_lock);
static u32 tcm_used;
static struct comm_kmem kmem_tcm;
static struct comm_kmem kmem_dram[MAX_VLM_CTX];
static struct mdla_vlm_ctx vlm_ctx[MAX_VLM_CTX];

static inline u32 mdla_get_swcmd(void *base_kva, u32 offset)
{
	return (*(u32 *)(base_kva + offset));
}

static inline void mdla_set_swcmd(void *base_kva, u32 offset, u32 val)
{
	(*(u32 *)(base_kva + offset)) = val;
}

static void backup_vlm_info(struct command_entry *ce, struct mdla_vlm_ctx *ctx)
{
	void *cmd_kva = NULL;
	uint32_t i = 0;
	u32 mask;

	if (ctx->cmd_vlm_backup1 == NULL ||
		ctx->cmd_vlm_backup2 == NULL)
		return;

	mask = kmem_tcm.size - 1;

	for (i = 0; i < ce->count; i++) {
		cmd_kva = ce->kva + i * MREG_CMD_SIZE;

		ctx->cmd_vlm_backup1[i] =
			mdla_get_swcmd(cmd_kva, BACKUP_ADDR1);

		if ((ctx->cmd_vlm_backup1[i] & ~mask) == kmem_tcm.iova)
			mdla_set_swcmd(cmd_kva, BACKUP_ADDR1,
				ctx->mem->iova + (ctx->cmd_vlm_backup1[i] - kmem_tcm.iova));

		ctx->cmd_vlm_backup2[i] =
			mdla_get_swcmd(cmd_kva, BACKUP_ADDR2);

		if ((ctx->cmd_vlm_backup2[i] & ~mask) == kmem_tcm.iova)
			mdla_set_swcmd(cmd_kva, BACKUP_ADDR2,
				ctx->mem->iova + (ctx->cmd_vlm_backup2[i] - kmem_tcm.iova));
	}
}

static void restore_vlm_info(struct command_entry *ce, struct mdla_vlm_ctx *ctx)
{
	void *cmd_kva = NULL;
	uint32_t i = 0;

	if (ctx->cmd_vlm_backup1 == NULL ||
		ctx->cmd_vlm_backup2 == NULL)
		return;

	for (i = 0; i < ce->count; i++) {
		cmd_kva = ce->kva + i * MREG_CMD_SIZE;

		mdla_set_swcmd(cmd_kva, BACKUP_ADDR1, ctx->cmd_vlm_backup1[i]);
		mdla_set_swcmd(cmd_kva, BACKUP_ADDR2, ctx->cmd_vlm_backup2[i]);
	}
}

static int request_tcm(struct mdla_vlm_ctx *ctx, u32 request_size, bool force)
{
	spin_lock(&ctx_lock);
	if (!tcm_used) {
		tcm_used = 1;
		ctx->mem = &kmem_tcm;
		spin_unlock(&ctx_lock);
		return 0;
	}
	spin_unlock(&ctx_lock);

	return -1;
}

static int release_tcm(struct mdla_vlm_ctx *ctx)
{
	spin_lock(&ctx_lock);
	tcm_used = 0;
	ctx->mem = NULL;
	spin_unlock(&ctx_lock);

	return 0;
}

int mdla_plat_vlm_is_dram(void *data)
{
	struct mdla_vlm_ctx *ctx = (struct mdla_vlm_ctx *)data;
	int i;

	if (ctx == NULL)
		return 0;

	spin_lock(&ctx_lock);
	for (i = 0; i < MAX_VLM_CTX; i++) {
		if ((&vlm_ctx[i] == ctx) && ctx->used) {
			spin_unlock(&ctx_lock);
			return (ctx->mem == &kmem_dram[i]);
		}
	}
	spin_unlock(&ctx_lock);

	return 0;
}

static struct mdla_vlm_ctx *get_vlm_ctx(void)
{
	struct mdla_vlm_ctx *ctx = NULL;
	int i;

	spin_lock(&ctx_lock);

	for (i = 0; i < MAX_VLM_CTX; i++) {
		if (vlm_ctx[i].used == 0) {
			ctx = &vlm_ctx[i];
			ctx->mem = &kmem_dram[i];
			ctx->used = 1;
			break;
		}
	}

	spin_unlock(&ctx_lock);

	if (ctx && ctx->mem->iova == 0) {
		ctx->mem->size = kmem_tcm.size;
		ctx->mem->mem_type = APU_COMM_MEM_DRAM_DMA;
		if (ctx->mem->size == 0 || comm_util_get_cb()->alloc(ctx->mem)) {
			ctx->used = 0;
			return NULL;
		}
	}

	return ctx;
}

static void put_vlm_ctx(struct mdla_vlm_ctx *ctx)
{
	int i;

	spin_lock(&ctx_lock);

	for (i = 0; i < MAX_VLM_CTX; i++) {
		if (&vlm_ctx[i] == ctx) {
			ctx->used = 0;
			kfree(ctx->cmd_vlm_backup1);
			kfree(ctx->cmd_vlm_backup2);
			ctx->cmd_vlm_backup1 = NULL;
			ctx->cmd_vlm_backup2 = NULL;

			break;
		}
	}

	spin_unlock(&ctx_lock);
}

void *mdla_plat_vlm_ctx_get(struct command_entry *ce, u32 request_size, bool force)
{
	struct mdla_vlm_ctx *ctx = NULL;
	u32 *cmd_vlm_backup1 = NULL;
	u32 *cmd_vlm_backup2 = NULL;

	ctx = get_vlm_ctx();
	if (ctx == NULL)
		return NULL;

	if (request_tcm(ctx, request_size, force)) {
		if (!ce->secure) {
			cmd_vlm_backup1 = kvmalloc(ce->count * sizeof(uint32_t), GFP_KERNEL);
			cmd_vlm_backup2 = kvmalloc(ce->count * sizeof(uint32_t), GFP_KERNEL);

			if (!cmd_vlm_backup1 && !cmd_vlm_backup2) {
				kfree(cmd_vlm_backup1);
				kfree(cmd_vlm_backup2);
				put_vlm_ctx(ctx);

				return NULL;
			}

			spin_lock(&ctx_lock);
			ctx->cmd_vlm_backup1 = cmd_vlm_backup1;
			ctx->cmd_vlm_backup2 = cmd_vlm_backup2;
			spin_unlock(&ctx_lock);

			backup_vlm_info(ce, ctx);
		}
	}

	return ctx;
}

int mdla_plat_vlm_ctx_put(struct command_entry *ce, void *data)
{
	struct mdla_vlm_ctx *ctx = (struct mdla_vlm_ctx *)data;

	if (!ctx)
		return 0;

	if (!mdla_plat_vlm_is_dram(data))
		release_tcm(ctx);
	else
		restore_vlm_info(ce, ctx);

	put_vlm_ctx(ctx);

	return 0;
}

int mdla_plat_vlm_init(void)
{
	if (comm_util_get_cb()->vlm_info(&kmem_tcm)) {
		mdla_err("%s(): vlm_info fail\n", __func__);
		return -EFAULT;
	}

	memset(vlm_ctx, 0, sizeof(vlm_ctx));
	memset(kmem_dram, 0, sizeof(kmem_dram));

	return -EFAULT;
}
