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

#include <linux/module.h>
#include <linux/sched/clock.h>
#include <linux/mutex.h>
#include <linux/time64.h>
#include <drm/mtk_tv_drm.h>

#include "mtk_tv_drm_log.h"

//--------------------------------------------------------------------------------------------------
//  Local Defines
//--------------------------------------------------------------------------------------------------
#define MTK_DRM_MODEL MTK_DRM_MODEL_COMMON
#define ERR(fmt, args...) DRM_ERROR("[%s][%d] " fmt, __func__, __LINE__, ##args)
#define LOG(fmt, args...) DRM_INFO("[%s][%d] " fmt, __func__, __LINE__, ##args)

#define SYSFS_LOGLV(len, buf, name, level) \
	SYSFS_PRINT(len, buf, "  %-12s = %d", name, (bool)(mtk_drm_log_level & level))

#define LOGREC_BUFFER_SIZE	1024
#define LOGREC_MSG_SIZE		128

//--------------------------------------------------------------------------------------------------
//  Local Enum and Structures
//--------------------------------------------------------------------------------------------------
struct logrec {
	bool init;
	struct mutex mutex;
	uint16_t r_idx;
	uint16_t w_idx;
	bool full;
	uint8_t buffer[LOGREC_BUFFER_SIZE];
};

struct hdr {
	uint64_t ts_nsec;
	uint16_t len;
} __packed;

struct msg {
	struct hdr hdr;
	char text[LOGREC_MSG_SIZE];
};

//--------------------------------------------------------------------------------------------------
//  Global Variables
//--------------------------------------------------------------------------------------------------
uint32_t mtk_drm_log_level; // DRM_INFO is default disable
module_param_named(log_level, mtk_drm_log_level, int, 0644);

//--------------------------------------------------------------------------------------------------
//  Local Variables
//--------------------------------------------------------------------------------------------------
static struct logrec _logrec[MTK_TV_DRM_LOGREC_TYPE_MAX];

//--------------------------------------------------------------------------------------------------
//  Local Functions
//--------------------------------------------------------------------------------------------------
static inline int _logrec_init(struct logrec *rec)
{
	if (unlikely(rec->init == false)) {
		memset(rec, 0, sizeof(struct logrec));
		mutex_init(&rec->mutex);
		rec->init = true;
	}
	return 0;
}

// return free bytes
static inline int _logrec_get_empty(struct logrec *rec)
{
	int diff = rec->r_idx - rec->w_idx;

	if (diff == 0) // full or mpty
		return (rec->full) ? 0 : LOGREC_BUFFER_SIZE;

	return (LOGREC_BUFFER_SIZE + diff) % LOGREC_BUFFER_SIZE;
}

static inline void _logrec_add_tail(struct logrec *rec, uint8_t *buff, uint32_t size)
{
	if (rec->w_idx + size >= LOGREC_BUFFER_SIZE) {
		int copy_size = LOGREC_BUFFER_SIZE - rec->w_idx;

		memcpy(&rec->buffer[rec->w_idx], buff, copy_size);
		size -= copy_size;
		buff += copy_size;
		rec->w_idx = 0;
	}
	if (size > 0) {
		memcpy(&rec->buffer[rec->w_idx], buff, size);
		rec->w_idx += size;
	}
	if (rec->w_idx == rec->r_idx)
		rec->full = true;
}

static inline void _logrec_del_head(struct logrec *rec, uint32_t size)
{
	if (rec->r_idx + size >= LOGREC_BUFFER_SIZE) {
		int reset_size = LOGREC_BUFFER_SIZE - rec->r_idx;

		memset(&rec->buffer[rec->r_idx], 0, reset_size);
		size -= reset_size;
		rec->r_idx = 0;
	}
	if (size > 0) {
		memset(&rec->buffer[rec->r_idx], 0, size);
		rec->r_idx += size;
	}
	if (rec->full == true)
		rec->full = false;
}

// return last idx
static inline int _logrec_read(struct logrec *rec, uint16_t idx, uint8_t *buff, uint32_t size)
{
	if (unlikely(idx >= LOGREC_BUFFER_SIZE))
		return idx;

	if (idx + size >= LOGREC_BUFFER_SIZE) {
		int copy_size = LOGREC_BUFFER_SIZE - idx;

		memcpy(buff, &rec->buffer[idx], copy_size);
		size -= copy_size;
		buff += copy_size;
		idx = 0;
	}
	if (size > 0) {
		memcpy(buff, &rec->buffer[idx], size);
		idx += size;
	}
	return idx;
}

//--------------------------------------------------------------------------------------------------
//  Global Functions
//--------------------------------------------------------------------------------------------------
ssize_t log_level_show(struct device *dev,
	struct device_attribute *attr, char *buf)
{
	int len = 0;

	if ((dev == NULL) || (buf == NULL)) {
		ERR("Pointer is NULL!");
		return -EINVAL;
	}

	SYSFS_PRINT(len, buf, "DRM Info log status:");
	SYSFS_LOGLV(len, buf, "COMMON",       MTK_DRM_MODEL_COMMON);
	SYSFS_LOGLV(len, buf, "DISPLAY",      MTK_DRM_MODEL_DISPLAY);
	SYSFS_LOGLV(len, buf, "PANEL",        MTK_DRM_MODEL_PANEL);
	SYSFS_LOGLV(len, buf, "CONNECTOR",    MTK_DRM_MODEL_CONNECTOR);
	SYSFS_LOGLV(len, buf, "CRTC",         MTK_DRM_MODEL_CRTC);
	SYSFS_LOGLV(len, buf, "DRV",          MTK_DRM_MODEL_DRV);
	SYSFS_LOGLV(len, buf, "ENCODER",      MTK_DRM_MODEL_ENCODER);
	SYSFS_LOGLV(len, buf, "FB",           MTK_DRM_MODEL_FB);
	SYSFS_LOGLV(len, buf, "GEM",          MTK_DRM_MODEL_GEM);
	SYSFS_LOGLV(len, buf, "KMS",          MTK_DRM_MODEL_KMS);
	SYSFS_LOGLV(len, buf, "PATTERN",      MTK_DRM_MODEL_PATTERN);
	SYSFS_LOGLV(len, buf, "PLANE",        MTK_DRM_MODEL_PLANE);
	SYSFS_LOGLV(len, buf, "VIDEO",        MTK_DRM_MODEL_VIDEO);
	SYSFS_LOGLV(len, buf, "GOP",          MTK_DRM_MODEL_GOP);
	SYSFS_LOGLV(len, buf, "DEMURA",       MTK_DRM_MODEL_DEMURA);
	SYSFS_LOGLV(len, buf, "GAMMA",        MTK_DRM_MODEL_PNLGAMMA);
	SYSFS_LOGLV(len, buf, "PQMAP",        MTK_DRM_MODEL_PQMAP);
	SYSFS_LOGLV(len, buf, "AUTOGEN",      MTK_DRM_MODEL_AUTOGEN);
	SYSFS_LOGLV(len, buf, "TVDAC",        MTK_DRM_MODEL_TVDAC);
	SYSFS_LOGLV(len, buf, "METABUF",      MTK_DRM_MODEL_METABUF);
	SYSFS_LOGLV(len, buf, "FENCE",        MTK_DRM_MODEL_FENCE);
	SYSFS_LOGLV(len, buf, "PQU_METADATA", MTK_DRM_MODEL_PQU_METADATA);
	SYSFS_LOGLV(len, buf, "OLED",         MTK_DRM_MODEL_OLED);
	SYSFS_LOGLV(len, buf, "LDM",          MTK_DRM_MODEL_LD);
	SYSFS_LOGLV(len, buf, "GLOBAL_PQ",    MTK_DRM_MODEL_GLOBAL_PQ);
	SYSFS_LOGLV(len, buf, "SCRIPTMGT_ML", MTK_DRM_MODEL_SCRIPTMGT_ML);
	SYSFS_LOGLV(len, buf, "FRC",          MTK_DRM_MODEL_FRC);

	return (len >= PAGE_SIZE) ? -EINVAL : len;
}

ssize_t log_level_store(struct device *dev,
	struct device_attribute *attr, const char *buf, size_t count)
{
	unsigned long val;
	int err;

	if ((dev == NULL) || (buf == NULL)) {
		ERR("Pointer is NULL!");
		return -EINVAL;
	}

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

	mtk_drm_log_level = val;
	return count;
}

int mtk_tv_drm_logrec_write(enum MTK_TV_DRM_LOGREC_TYPE type, const char *format, ...)
{
	struct logrec *rec = NULL;
	struct hdr hdr = {0};
	struct msg msg = {0};
	va_list args;
	uint32_t size;
	int len;

	// check type
	if (type < 0 || type >= MTK_TV_DRM_LOGREC_TYPE_MAX)
		return -EINVAL;
	rec = &_logrec[type];

	// check init
	_logrec_init(rec);

	// setup msg
	va_start(args, format);
	len = vsnprintf(msg.text, LOGREC_MSG_SIZE, format, args) + 1; // add '\0'
	va_end(args);
	if (len < 0 || len >= LOGREC_MSG_SIZE)
		return -ENOMEM;

	msg.hdr.ts_nsec = local_clock();
	msg.hdr.len = len;

	size = sizeof(struct hdr) + len;

	mutex_lock(&rec->mutex);

	// ensure empty size
	while (_logrec_get_empty(rec) < size) {
		_logrec_read(rec, rec->r_idx, (uint8_t *)&hdr, sizeof(struct hdr));
		_logrec_del_head(rec, sizeof(struct hdr) + hdr.len);
	}

	// write newest msg
	_logrec_add_tail(rec, (uint8_t *)&msg, size);

	mutex_unlock(&rec->mutex);
	return 0;
}

int mtk_tv_drm_logrec_read(enum MTK_TV_DRM_LOGREC_TYPE type, char *buffer, uint32_t *length)
{
	struct logrec *rec = NULL;
	struct msg msg = {0};
	uint64_t sec, us;
	uint32_t idx;
	int len;

	// check input
	if (type < 0 || type >= MTK_TV_DRM_LOGREC_TYPE_MAX || buffer == NULL || length == NULL)
		return -EINVAL;
	rec = &_logrec[type];
	len = *length;

	// check init
	_logrec_init(rec);

	mutex_lock(&rec->mutex);

	idx = rec->r_idx;
	do {
		idx = _logrec_read(rec, idx, (uint8_t *)&msg.hdr, sizeof(struct hdr));
		if (msg.hdr.ts_nsec == 0 || msg.hdr.len >= LOGREC_MSG_SIZE)
			break;

		idx = _logrec_read(rec, idx, (uint8_t *)msg.text, msg.hdr.len);
		sec = msg.hdr.ts_nsec;
		us = do_div(sec, NSEC_PER_SEC) / NSEC_PER_USEC;
		SYSFS_PRINT(len, buffer, "[%5llu.%06llu] %s", sec, us, msg.text);
	} while (idx != rec->w_idx && idx < LOGREC_BUFFER_SIZE); // last msg

	mutex_unlock(&rec->mutex);

	*length = len;
	return 0;
}
