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

#include <linux/module.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <linux/component.h>
#include <drm/mtk_tv_drm.h>

#include "mtk_tv_drm_kms.h"
#include "mtk_tv_drm_common.h"
#include "mtk_tv_drm_log.h"
#include "mtk_tv_drm_pqu_agent.h"
#include "mtk_tv_drm_pnlgamma.h"
#include "m_pqu_render.h"
#include "pqu_render.h"
#include "hwreg_render_pnlgamma.h"

#if MTK_DRM_PANEL_GAMMA_CURVE_LENGTH != PQU_RENDER_PNLGAMMA_CURVE_SIZE_MAX
#error "Panel Gamma size not match"
#endif

//--------------------------------------------------------------------------------------------------
//  Local Defines
//--------------------------------------------------------------------------------------------------
#define MTK_DRM_MODEL	MTK_DRM_MODEL_PNLGAMMA
#define LOG_PRINT(pr_func, fmt, args...) ({					\
	pr_func("[%s:%d] " fmt, __func__, __LINE__, ##args);			\
	mtk_tv_drm_logrec_write(MTK_TV_DRM_LOGREC_PANEL_GAMMA, "[%s] " fmt,	\
		__func__ + 22 /* ignore "mtk_video_panel_gamma_" */, ##args);	\
	})
#define ERR(fmt, args...) LOG_PRINT(DRM_ERROR, fmt, ##args)
#define WRN(fmt, args...) LOG_PRINT(DRM_WARN, fmt, ##args)
#define LOG(fmt, args...) LOG_PRINT(DRM_INFO, fmt, ##args)

#define CURVE_CPY(_dst, _src, _size) memcpy(_dst, _src, _size * sizeof(uint16_t))

#define IMPELMENT_PNLGAMMA_IOCTL(_action, _param_type)				\
int mtk_tv_drm_pnlgamma_## _action ##_ioctl(struct drm_device *drm_dev,		\
	void *data, struct drm_file *file_priv)					\
{										\
	struct mtk_tv_kms_context *kms = NULL;					\
	struct drm_crtc *crtc = NULL;						\
	struct mtk_tv_drm_crtc *mtk_tv_crtc = NULL;				\
	_param_type *param = NULL;						\
	if (!drm_dev || !data || !file_priv) {					\
		ERR("Invalid input");						\
		return -EINVAL;							\
	}									\
	drm_for_each_crtc(crtc, drm_dev) {					\
		mtk_tv_crtc = to_mtk_tv_crtc(crtc);				\
		break;								\
	}									\
	kms = (struct mtk_tv_kms_context *)mtk_tv_crtc->crtc_private;		\
	param = (_param_type *)data;						\
	return mtk_video_panel_gamma_## _action(kms, param);			\
}

#define SYSFS_SHOW_CURVE_DIFF 31

//--------------------------------------------------------------------------------------------------
//  Local Enum and Structures
//--------------------------------------------------------------------------------------------------
struct pnlgamma_info {
	int init;

	struct mtk_tv_drm_metabuf metabuf;
	struct meta_render_pnlgamma_info *pnlgamma_shm;
	uint64_t shm_addr;
};

//--------------------------------------------------------------------------------------------------
//  Local Variables
//--------------------------------------------------------------------------------------------------
static struct pnlgamma_info _pnlgamma_info;

//--------------------------------------------------------------------------------------------------
//  Local Functions
//--------------------------------------------------------------------------------------------------
static inline int _mtk_video_panel_gamma_update(struct mtk_tv_kms_context *kms,
	struct pqu_render_pnlgamma *pnlgamma)
{
	struct pnlgamma_info *info = &_pnlgamma_info;
	struct pqu_render_agent_param agent;
	int ret = 0;

	// fill common parameters
	pnlgamma->size = sizeof(struct meta_render_pnlgamma_info);
	pnlgamma->addr = info->shm_addr;
	pnlgamma->stub = kms->stub;

	// fill agent parameters
	memset(&agent, 0, sizeof(struct pqu_render_agent_param));
	agent.pnlgamma_param = *pnlgamma;

	if (kms->stub) {
		ret = mtk_tv_drm_pqu_agent_sync_reply(E_PQU_RENDER_AGENT_CMD_PNLGAMMA,
							&agent, &agent);
		if (ret || agent.result) {
			if (ret) {
				ERR("pqu agent fail, ret=%d", ret);
			} else {
				ERR("pqu_pnlgamma fail, ret=%d", agent.result);
				ret = agent.result;
			}
		}
	} else {
		ret = mtk_tv_drm_pqu_agent_no_reply(E_PQU_RENDER_AGENT_CMD_PNLGAMMA, &agent);
		if (ret)
			ERR("pqu agent fail, ret=%d", ret);
	}
	return ret;
}

//--------------------------------------------------------------------------------------------------
//  Global Functions
//--------------------------------------------------------------------------------------------------
int mtk_video_panel_gamma_init(struct mtk_tv_kms_context *kms)
{
	struct pnlgamma_info *info = &_pnlgamma_info;
	struct mtk_tv_drm_metabuf metabuf;

	if (!kms) {
		ERR("invalid input, kms:%p", kms);
		return -EINVAL;
	}
	if (kms->panel_priv.vrr_pga_En || kms->panel_priv.cus.pgamma_en) {
		WRN("tcon panel gamma is enable (pga:%d, vrr:%d)", kms->panel_priv.cus.pgamma_en,
								kms->panel_priv.vrr_pga_En);
		return 0;
	}
	if (info->init > 0)
		return 0;

	if (info->init < 0) {
		ERR("has inited fail, ret=%d", info->init);
		return info->init;
	}

	memset(&metabuf, 0, sizeof(struct mtk_tv_drm_metabuf));
	if (mtk_tv_drm_metabuf_alloc_by_mmap(&metabuf, MTK_TV_DRM_METABUF_MMAP_TYPE_PQU_PNLGAMMA)) {
		ERR("get mmap fail");
		info->init = -EINVAL;
		return -EINVAL;
	}
	memset(metabuf.addr, 0, sizeof(struct meta_render_pnlgamma_info));
	info->metabuf = metabuf;
	info->pnlgamma_shm = metabuf.addr;
	info->pnlgamma_shm->hw_version = kms->globalpq_hw_ver.pnlgamma_version;
	info->shm_addr = metabuf.mmap_info.bus_addr;
	info->init = true;
	return 0;
}

int mtk_video_panel_gamma_deinit(struct mtk_tv_kms_context *kms)
{
	struct pnlgamma_info *info = &_pnlgamma_info;

	if (info->init > 0) {
		mtk_tv_drm_metabuf_free(&info->metabuf);
		memset(info, 0, sizeof(struct pnlgamma_info));
	}
	return 0;
}

int mtk_video_panel_gamma_resume(struct mtk_tv_kms_context *kms)
{
	struct pnlgamma_info *info = &_pnlgamma_info;
	struct pqu_render_pnlgamma pnlgamma;

	if (!kms) {
		ERR("invalid input, kms:%p", kms);
		return -EINVAL;
	}
	if (kms->panel_priv.vrr_pga_En || kms->panel_priv.cus.pgamma_en) {
		WRN("tcon panel gamma is enable (pga:%d, vrr:%d)", kms->panel_priv.cus.pgamma_en,
								kms->panel_priv.vrr_pga_En);
		return 0;
	}
	if (info->init <= 0) {
		ERR("not inited");
		return (info->init) ? info->init : -EINVAL;
	}

	// setup updated flag
	memset(&pnlgamma, 0, sizeof(struct pqu_render_pnlgamma));
	pnlgamma.update_curve = true;
	pnlgamma.update_curve_enable = true;
	pnlgamma.update_gainoffset = true;
	pnlgamma.update_gainoffset_enable = true;
	return _mtk_video_panel_gamma_update(kms, &pnlgamma);
}

int mtk_video_panel_gamma_set_curve(
	struct mtk_tv_kms_context *kms,
	uint16_t *r,
	uint16_t *g,
	uint16_t *b,
	uint16_t size)
{
	struct pnlgamma_info *info = &_pnlgamma_info;
	struct pqu_render_pnlgamma pnlgamma;

	if (!kms || !r || !g || !b || size != MTK_DRM_PANEL_GAMMA_CURVE_LENGTH) {
		ERR("invalid input, kms:%p, size:%u, r:%p, g:%p, b:%p", kms, size, r, g, b);
		return -EINVAL;
	}
	if (kms->panel_priv.vrr_pga_En || kms->panel_priv.cus.pgamma_en) {
		WRN("tcon panel gamma is enable (pga:%d, vrr:%d)", kms->panel_priv.vrr_pga_En,
								kms->panel_priv.cus.pgamma_en);
		return -ENODEV;
	}
	if (info->init <= 0) {
		ERR("not inited");
		return (info->init) ? info->init : -EINVAL;
	}

	LOG("curve size:%u, 0(%u, %u, %u), %u(%u, %u, %u), %u(%u, %u, %u)", size, r[0], g[0], b[0],
		size >> 1, r[size >> 1], g[size >> 1], b[size >> 1],
		size - 1, r[size - 1], g[size - 1], b[size - 1]);

	// setup shared memory
	info->pnlgamma_shm->curve_size = MTK_DRM_PANEL_GAMMA_CURVE_LENGTH;
	CURVE_CPY(info->pnlgamma_shm->curve_r, r, MTK_DRM_PANEL_GAMMA_CURVE_LENGTH);
	CURVE_CPY(info->pnlgamma_shm->curve_g, g, MTK_DRM_PANEL_GAMMA_CURVE_LENGTH);
	CURVE_CPY(info->pnlgamma_shm->curve_b, b, MTK_DRM_PANEL_GAMMA_CURVE_LENGTH);

	// setup updated flag
	memset(&pnlgamma, 0, sizeof(struct pqu_render_pnlgamma));
	pnlgamma.update_curve = true;
	return _mtk_video_panel_gamma_update(kms, &pnlgamma);
}

int mtk_video_panel_gamma_get_curve(
	struct mtk_tv_kms_context *kms,
	struct drm_mtk_tv_pnlgamma_curve *curve)
{
	uint32_t i;
	int ret = 0;

	if (!kms || !curve) {
		ERR("invalid input, kms:%p, curve:%p", kms, curve);
		return -EINVAL;
	}
	if (kms->panel_priv.vrr_pga_En || kms->panel_priv.cus.pgamma_en) {
		WRN("tcon panel gamma is enable (pga:%d, vrr:%d)", kms->panel_priv.cus.pgamma_en,
								kms->panel_priv.vrr_pga_En);
		return -ENODEV;
	}

	// get curve
	ret = drv_hwreg_render_pnlgamma_get_curve(curve->curve_r, curve->curve_g, curve->curve_b,
						MTK_DRM_PANEL_GAMMA_CURVE_LENGTH);
	if (ret) {
		ERR("hwreg get curve fail, ret=%d", ret);
		return ret;
	}
	curve->size = MTK_DRM_PANEL_GAMMA_CURVE_LENGTH;

	LOG("Panel Gamma Curve (Red, Green, Blue)");
	for (i = 0; i < curve->size; ++i) {
		LOG("%-4d) 0x%03X 0x%03X 0x%03X", i,
			curve->curve_r[i], curve->curve_g[i], curve->curve_b[i]);
	}
	return ret;

}

int mtk_video_panel_gamma_set_curve_enable(
	struct mtk_tv_kms_context *kms,
	struct drm_mtk_tv_pnlgamma_enable *param)
{
	struct pnlgamma_info *info = &_pnlgamma_info;
	struct pqu_render_pnlgamma pnlgamma;

	if (!kms || !param) {
		ERR("invalid input, kms:%p, param:%p", kms, param);
		return -EINVAL;
	}
	if (kms->panel_priv.vrr_pga_En || kms->panel_priv.cus.pgamma_en) {
		WRN("tcon panel gamma is enable (pga:%d, vrr:%d)", kms->panel_priv.cus.pgamma_en,
								kms->panel_priv.vrr_pga_En);
		return -ENODEV;
	}
	if (info->init <= 0) {
		ERR("not inited");
		return (info->init) ? info->init : -EINVAL;
	}

	LOG("curve enable:%u", param->enable);

	// setup shared memory
	info->pnlgamma_shm->curve_enable = param->enable;

	// setup updated flag
	memset(&pnlgamma, 0, sizeof(struct pqu_render_pnlgamma));
	pnlgamma.update_curve_enable = true;
	return _mtk_video_panel_gamma_update(kms, &pnlgamma);
}

int mtk_video_panel_gamma_get_curve_enable(
	struct mtk_tv_kms_context *kms,
	struct drm_mtk_tv_pnlgamma_enable *param)
{
	struct pnlgamma_info *info = &_pnlgamma_info;

	if (!kms || !param) {
		ERR("invalid input, kms:%p, param:%p", kms, param);
		return -EINVAL;
	}
	if (kms->panel_priv.vrr_pga_En || kms->panel_priv.cus.pgamma_en) {
		WRN("tcon panel gamma is enable (pga:%d, vrr:%d)", kms->panel_priv.cus.pgamma_en,
								kms->panel_priv.vrr_pga_En);
		return -ENODEV;
	}
	if (info->init <= 0) {
		ERR("not inited");
		return (info->init) ? info->init : -EINVAL;
	}

	// read shared memory data
	param->enable = info->pnlgamma_shm->curve_enable;

	return 0;
}

int mtk_video_panel_gamma_set_gainoffset(
	struct mtk_tv_kms_context *kms,
	struct drm_mtk_tv_pnlgamma_gainoffset *param)
{
	struct pnlgamma_info *info = &_pnlgamma_info;
	struct pqu_render_pnlgamma pnlgamma;

	if (!kms || !param) {
		ERR("invalid input, kms:%p, param:%p", kms, param);
		return -EINVAL;
	}
	if (kms->panel_priv.vrr_pga_En || kms->panel_priv.cus.pgamma_en) {
		WRN("tcon panel gamma is enable (pga:%d, vrr:%d)", kms->panel_priv.cus.pgamma_en,
								kms->panel_priv.vrr_pga_En);
		return -ENODEV;
	}
	if (info->init <= 0) {
		ERR("not inited");
		return (info->init) ? info->init : -EINVAL;
	}

	LOG("set offset(r:%u, g:%u, b:%u), gain(r:%u, g:%u, b:%u)",
		param->pnlgamma_offset_r, param->pnlgamma_offset_g, param->pnlgamma_offset_b,
		param->pnlgamma_gain_r, param->pnlgamma_gain_g, param->pnlgamma_gain_b);

	// setup shared memory
	info->pnlgamma_shm->gain_r   = param->pnlgamma_gain_r;
	info->pnlgamma_shm->gain_g   = param->pnlgamma_gain_g;
	info->pnlgamma_shm->gain_b   = param->pnlgamma_gain_b;
	info->pnlgamma_shm->offset_r = param->pnlgamma_offset_r;
	info->pnlgamma_shm->offset_g = param->pnlgamma_offset_g;
	info->pnlgamma_shm->offset_b = param->pnlgamma_offset_b;

	// setup updated flag
	memset(&pnlgamma, 0, sizeof(struct pqu_render_pnlgamma));
	pnlgamma.update_gainoffset = true;
	return _mtk_video_panel_gamma_update(kms, &pnlgamma);
}

int mtk_video_panel_gamma_set_gainoffset_enable(
	struct mtk_tv_kms_context *kms,
	struct drm_mtk_tv_pnlgamma_gainoffset_enable *param)
{
	struct pnlgamma_info *info = &_pnlgamma_info;
	struct pqu_render_pnlgamma pnlgamma;

	if (!kms || !param) {
		ERR("invalid input, kms:%p, param:%p", kms, param);
		return -EINVAL;
	}
	if (kms->panel_priv.vrr_pga_En || kms->panel_priv.cus.pgamma_en) {
		WRN("tcon panel gamma is enable (pga:%d, vrr:%d)", kms->panel_priv.cus.pgamma_en,
								kms->panel_priv.vrr_pga_En);
		return -ENODEV;
	}
	if (info->init <= 0) {
		ERR("not inited");
		return (info->init) ? info->init : -EINVAL;
	}

	LOG("gainoffset enable:%u", param->enable);

	// setup shared memory
	info->pnlgamma_shm->gainoffset_enbale = param->enable;

	// setup updated flag
	memset(&pnlgamma, 0, sizeof(struct pqu_render_pnlgamma));
	pnlgamma.update_gainoffset_enable = true;
	return _mtk_video_panel_gamma_update(kms, &pnlgamma);
}

// sysfs
ssize_t panel_gamma_show(struct device *dev,
	struct device_attribute *attr, char *buf)
{
	struct pnlgamma_info *info = &_pnlgamma_info;
	struct meta_render_pnlgamma_info *shm = info->pnlgamma_shm;
	struct mtk_tv_kms_context *kms = dev_get_drvdata(dev);
	uint16_t i;
	int len = 0;

	SYSFS_PRINT(len, buf, "=== Log ===");
	mtk_tv_drm_logrec_read(MTK_TV_DRM_LOGREC_PANEL_GAMMA, buf, &len);

	SYSFS_PRINT(len, buf, "=== Env ===");

	SYSFS_PRINT(len, buf, "Tcon PGA");
	SYSFS_PRINT(len, buf, "  enable: %u", kms->panel_priv.cus.pgamma_en);
	SYSFS_PRINT(len, buf, "  vrr   : %u", kms->panel_priv.vrr_pga_En);

	if (shm == NULL) {
		SYSFS_PRINT(len, buf, "Not support USER Panel Gamma");
		goto EXIT;
	}

	SYSFS_PRINT(len, buf, "Gainoffset");
	SYSFS_PRINT(len, buf, "  enable: %u", shm->gainoffset_enbale);
	SYSFS_PRINT(len, buf, "  gain  : r=%4u, g=%4u, b=%4u",
				shm->gain_r, shm->offset_g, shm->gain_b);
	SYSFS_PRINT(len, buf, "  offset: r=%4u, g=%4u, b=%4u",
				shm->offset_r, shm->offset_g, shm->offset_b);

	SYSFS_PRINT(len, buf, "Curve");
	SYSFS_PRINT(len, buf, "  enable: %u", shm->curve_enable);
	for (i = 0; i < PQU_RENDER_PNLGAMMA_CURVE_SIZE_MAX; i += SYSFS_SHOW_CURVE_DIFF)
		SYSFS_PRINT(len, buf, "  %4u  : r=%4u, g=%4u, b=%4u", i,
			shm->curve_r[i], shm->curve_g[i], shm->curve_b[i]);

EXIT:
	return len;
}

ssize_t panel_gamma_store(struct device *dev,
	struct device_attribute *attr, const char *buf, size_t count)
{
	unsigned long val = 0;
	int err;
	bool en;

	err = kstrtoul(buf, 0, &val);
	if (err)
		return err;

	en = (bool)val;
	pr_emerg("panel amma is %s\n", (en) ? "enabled" : "disabled");

	drv_hwreg_render_pnlgamma_bypass(!en);

	return count;
}

ssize_t panel_gamma_curve_store(struct device *dev,
	struct device_attribute *attr, const char *buf, size_t count)
{
	struct mtk_tv_kms_context *kms = dev_get_drvdata(dev);
	struct drm_mtk_tv_pnlgamma_curve *cv = NULL;
	struct drm_mtk_tv_pnlgamma_enable en;
	uint64_t enable = 0, start = 0, diff = 0;
	uint16_t i;

	if (!mtk_render_common_parser_cmd_int(buf, "start", &start) &&
	!mtk_render_common_parser_cmd_int(buf, "diff", &diff)) {
		cv = MALLOC(sizeof(struct drm_mtk_tv_pnlgamma_curve));
		if (cv) {
			cv->size = MTK_DRM_PANEL_GAMMA_CURVE_LENGTH;
			for (i = 0; i < MTK_DRM_PANEL_GAMMA_CURVE_LENGTH; i++)
				cv->curve_r[i] = cv->curve_g[i] = cv->curve_b[i] = start + i * diff;
			mtk_video_panel_gamma_set_curve(kms, cv->curve_r, cv->curve_g, cv->curve_b,
											cv->size);
			FREE(cv);
		}
	}
	if (!mtk_render_common_parser_cmd_int(buf, "enable", &enable)) {
		// setup curve enable
		memset(&en, 0, sizeof(struct drm_mtk_tv_pnlgamma_enable));
		en.enable = enable;
		mtk_video_panel_gamma_set_curve_enable(kms, &en);
	}
	return count;
}

ssize_t panel_gamma_gainoffset_store(struct device *dev,
	struct device_attribute *attr, const char *buf, size_t count)
{
	struct mtk_tv_kms_context *kms = dev_get_drvdata(dev);
	struct drm_mtk_tv_pnlgamma_gainoffset param;
	struct drm_mtk_tv_pnlgamma_gainoffset_enable en;
	uint64_t enable = 0, gain = 0, offset = 0;

	if (!mtk_render_common_parser_cmd_int(buf, "gain", &gain) &&
	!mtk_render_common_parser_cmd_int(buf, "offset", &offset)) {
		// setup gainoffset
		memset(&param, 0, sizeof(struct drm_mtk_tv_pnlgamma_gainoffset));
		param.pnlgamma_gain_r = param.pnlgamma_gain_g = param.pnlgamma_gain_b = gain;
		param.pnlgamma_offset_r = param.pnlgamma_offset_g = param.pnlgamma_offset_b
											= offset;
		mtk_video_panel_gamma_set_gainoffset(kms, &param);
	}
	if (!mtk_render_common_parser_cmd_int(buf, "enable", &enable)) {
		// setup gainoffset_enable
		memset(&en, 0, sizeof(struct drm_mtk_tv_pnlgamma_gainoffset_enable));
		en.enable = enable;
		mtk_video_panel_gamma_set_gainoffset_enable(kms, &en);
	}
	return count;
}

// ioctl
IMPELMENT_PNLGAMMA_IOCTL(get_curve, struct drm_mtk_tv_pnlgamma_curve)
IMPELMENT_PNLGAMMA_IOCTL(set_curve_enable, struct drm_mtk_tv_pnlgamma_enable)
IMPELMENT_PNLGAMMA_IOCTL(get_curve_enable, struct drm_mtk_tv_pnlgamma_enable)
IMPELMENT_PNLGAMMA_IOCTL(set_gainoffset, struct drm_mtk_tv_pnlgamma_gainoffset)
IMPELMENT_PNLGAMMA_IOCTL(set_gainoffset_enable, struct drm_mtk_tv_pnlgamma_gainoffset_enable)
