// SPDX-License-Identifier: (GPL-2.0+ OR MIT)
/*
 * Copyright (c) 2019 Amlogic, Inc. All rights reserved.
 */

#include <linux/version.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/irq.h>
#include <linux/types.h>
#include <linux/input.h>
#include <linux/kernel.h>
#include <linux/kthread.h>
#include <linux/delay.h>
#include <linux/debugfs.h>
#include <linux/interrupt.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/mm.h>
#include <linux/major.h>
#include <linux/platform_device.h>
#include <linux/mutex.h>
#include <linux/cdev.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/list.h>
#include <linux/spinlock.h>
#include <linux/spinlock_types.h>
#include <linux/extcon-provider.h>
#include "hdmi_tx_module.h"
#include <linux/uaccess.h>
#include "hw/common.h"
#include "hdmi_tx_hdcp.h"
/*
 * hdmi_tx_hdcp.c
 * version 1.1
 */

static int hdmi_authenticated;

static DEFINE_MUTEX(mutex);
/* obs_cur, record current obs */
static struct hdcp_obs_val obs_cur;
/* if obs_cur is differ than last time, then save to obs_last */
static struct hdcp_obs_val obs_last;
static int write_buff(const char *fmt, ...);

static struct hdcplog_buf hdcplog_buf;

unsigned int hdcp_get_downstream_ver(struct hdmitx_dev *hdev)
{
	unsigned int ret = 14;

	/* if TX don't have HDCP22 key, skip RX hdcp22 ver */
	if (hdmitx_hw_cntl_ddc(&hdev->tx_hw.base, DDC_HDCP_22_LSTORE, 0) == 0)
		if (hdcp_rd_hdcp22_ver())
			ret = 22;
		else
			ret = 14;
	else
		ret = 14;
	return ret;
}

/* Notic: the HDCP key setting has been moved to uboot
 * On MBX project, it is too late for HDCP get from
 * other devices
 */

/* verify ksv, 20 ones and 20 zeroes */
int hdcp_ksv_valid(unsigned char *dat)
{
	int i, j, one_num = 0;

	for (i = 0; i < 5; i++) {
		for (j = 0; j < 8; j++) {
			if ((dat[i] >> j) & 0x1)
				one_num++;
		}
	}
	return one_num == 20;
}

static int save_obs_val(struct hdmitx_dev *hdev, struct hdcp_obs_val *obs)
{
	return hdmitx_hw_cntl_ddc(&hdev->tx_hw.base,
		DDC_HDCP14_SAVE_OBS, (unsigned long)obs);
}

static void pr_obs(struct hdcp_obs_val *obst0, struct hdcp_obs_val *obst1,
	unsigned int mask)
{
	/* if idx > HDCP_LOG_SIZE, then set idx as 0 for wrapping log */
#define GETBITS(val, start, len) (((val) >> (start)) & ((1 << (len)) - 1))
#define DIFF_ST(_v1, v0, _s, _l, str) \
	do { \
		typeof(_v1) v1 = (_v1); \
		typeof(_s) s = (_s); \
		typeof(_l) l = (_l); \
		if (GETBITS(v1, s, l) != (GETBITS(v0, s, l))) { \
			write_buff("%s: %x\n", str, GETBITS(v1, s, l)); \
		} \
	} while (0)

	if (mask & (1 << 0)) {
		if (GETBITS(obst1->obs0, 1, 7) != GETBITS(obst0->obs0, 1, 7)) {
			write_buff("StateA: %x  SubStateA: %x\n",
				GETBITS(obst1->obs0, 4, 4),
				GETBITS(obst1->obs0, 1, 3));
		}
		DIFF_ST(obst1->obs0, obst0->obs0, 0, 1, "hdcpengaged");
	}
	if (mask & (1 << 1)) {
		DIFF_ST(obst1->obs1, obst0->obs1, 4, 3, "StateOEG");
		DIFF_ST(obst1->obs1, obst0->obs1, 0, 4, "StateR");
	}
	if (mask & (1 << 2)) {
		DIFF_ST(obst1->obs2, obst0->obs2, 3, 3, "StateE");
		DIFF_ST(obst1->obs2, obst0->obs2, 0, 3, "StateEEG");
	}
	if (mask & (1 << 3)) {
		DIFF_ST(obst1->obs3, obst0->obs3, 7, 1, "RSVD");
		DIFF_ST(obst1->obs3, obst0->obs3, 6, 1, "Repeater");
		DIFF_ST(obst1->obs3, obst0->obs3, 5, 1, "KSV_FIFO_Ready");
		DIFF_ST(obst1->obs3, obst0->obs3, 4, 1, "Fast_i2c");
		DIFF_ST(obst1->obs3, obst0->obs3, 3, 1, "RSVD2");
		DIFF_ST(obst1->obs3, obst0->obs3, 2, 1, "Hdmi_mode");
		DIFF_ST(obst1->obs3, obst0->obs3, 1, 1, "Features1.1");
		DIFF_ST(obst1->obs3, obst0->obs3, 0, 1, "Fast_Reauth");
	}
	if (mask & (1 << 4)) {
		DIFF_ST(obst1->intstat, obst0->intstat, 7, 1, "hdcp_engaged");
		DIFF_ST(obst1->intstat, obst0->intstat, 6, 1, "hdcp_failed");
		DIFF_ST(obst1->intstat, obst0->intstat, 4, 1, "i2cnack");
		DIFF_ST(obst1->intstat, obst0->intstat, 3, 1,
			"lostarbitration");
		DIFF_ST(obst1->intstat, obst0->intstat, 2, 1,
			"keepouterrorint");
		DIFF_ST(obst1->intstat, obst0->intstat, 1, 1, "KSVsha1calcint");
	}
}

static void _hdcp_do_work(struct work_struct *work)
{
	int ret = 0;
	struct hdmitx_dev *hdev =
		container_of(work, struct hdmitx_dev, work_do_hdcp.work);

	switch (hdev->tx_comm.hdcp_mode) {
	case 2:
		/* hdev->HWOp.CntlMisc(hdev, MISC_HDCP_CLKDIS, 1); */
		/* schedule_delayed_work(&hdev->work_do_hdcp, HZ / 50); */
		break;
	case 1:
		mutex_lock(&mutex);
		ret = save_obs_val(hdev, &obs_cur);
		/* ret is NZ, then update obs_last */
		if (ret) {
			pr_obs(&obs_last, &obs_cur, ret);
			obs_last = obs_cur;
		}
		mutex_unlock(&mutex);
		/* log time frequency */
		schedule_delayed_work(&hdev->work_do_hdcp, HZ / 20);
		break;
	case 0:
	default:
		mutex_lock(&mutex);
		memset(&obs_cur, 0, sizeof(obs_cur));
		memset(&obs_last, 0, sizeof(obs_last));
		mutex_unlock(&mutex);
		hdmitx_hw_cntl_misc(&hdev->tx_hw.base, MISC_HDCP_CLKDIS, 0);
		break;
	}
}

void hdmitx_hdcp_do_work(struct hdmitx_dev *hdev)
{
	_hdcp_do_work(&hdev->work_do_hdcp.work);
}

static int hdmitx_hdcp_task(void *data)
{
	static int auth_trigger;
	struct hdmitx_dev *hdev = (struct hdmitx_dev *)data;

	INIT_DELAYED_WORK(&hdev->work_do_hdcp, _hdcp_do_work);
	while (hdev->hpd_event != 0xff) {
		hdmi_authenticated = hdmitx_hw_cntl_ddc(&hdev->tx_hw.base,
			DDC_HDCP_GET_AUTH, 0);
		hdmitx_hdcp_status(hdmi_authenticated);
		if (auth_trigger != hdmi_authenticated) {
			auth_trigger = hdmi_authenticated;
			HDMITX_INFO("hdcptx: %d  auth: %d\n", hdev->tx_comm.hdcp_mode,
			auth_trigger);
			// Only collect the metric when hdmi is plugged in.
			if (hdev->tx_comm.hpd_state == 1) {
				hdmitx_current_status(auth_trigger
					? HDMITX_HDCP_AUTH_SUCCESS
					: HDMITX_HDCP_AUTH_FAILURE);
			}
		}
		msleep_interruptible(200);
	}

	return 0;
}

static int read_buff(char *p)
{
	if (hdcplog_buf.rd_pos == hdcplog_buf.wr_pos)
		return 0;

	*p = hdcplog_buf.buf[hdcplog_buf.rd_pos];
	hdcplog_buf.rd_pos = (hdcplog_buf.rd_pos + 1) % HDCP_LOG_SIZE;
	return 1;
}

static void _write_buff(char c)
{
	hdcplog_buf.buf[hdcplog_buf.wr_pos] = c;
	hdcplog_buf.wr_pos = (hdcplog_buf.wr_pos + 1) % HDCP_LOG_SIZE;
	if (hdcplog_buf.wr_pos == hdcplog_buf.rd_pos)
		hdcplog_buf.rd_pos = (hdcplog_buf.rd_pos + 1) % HDCP_LOG_SIZE;
	wake_up_interruptible(&hdcplog_buf.wait);
}

static int write_buff(const char *fmt, ...)
{
	va_list args;
	int i, len;
	static char temp[64];

	va_start(args, fmt);
	len = vsnprintf(temp, sizeof(temp), fmt, args);
	va_end(args);

	for (i = 0; i < len; i++)
		_write_buff(temp[i]);

	return len;
}

static int hdcplog_open(struct inode *inode, struct file *file)
{
	return 0;
}

static ssize_t hdcplog_read(struct file *file, char __user *buf,
			size_t count, loff_t *ppos)
{
	ssize_t error = -EINVAL;
	int i = 0;
	char c;

	if ((file->f_flags & O_NONBLOCK) && hdcplog_buf.rd_pos == hdcplog_buf.wr_pos)
		return  -EAGAIN;

	if (!buf || !count)
		goto out;

	error = wait_event_interruptible(hdcplog_buf.wait,
			(hdcplog_buf.wr_pos != hdcplog_buf.rd_pos));
	if (error)
		goto out;
	while (!error && (read_buff(&c)) && i < count) {
		error = __put_user(c, buf);
		buf++;
		i++;
	}

	if (!error)
		error = i;
out:
	return error;
}

const struct file_operations hdcplog_ops = {
	.read = hdcplog_read,
	.open = hdcplog_open,
};

static struct dentry *hdmitx_dbgfs;

int hdmitx_hdcp_init(struct hdmitx_dev *hdev)
{
	struct dentry *entry;

	pr_debug(HDCP "%s\n", __func__);
	if (!hdev->hdtx_dev) {
		HDMITX_DEBUG_HDCP("exit for null device of hdmitx!\n");
		return -ENODEV;
	}

	memset(&hdcplog_buf, 0, sizeof(hdcplog_buf));
	init_waitqueue_head(&hdcplog_buf.wait);

	hdev->task_hdcp = kthread_run(hdmitx_hdcp_task,	(void *)hdev,
				      "kthread_hdcp");

	hdmitx_dbgfs = hdmitx_get_dbgfsdentry();
	if (!hdmitx_dbgfs)
		hdmitx_dbgfs = debugfs_create_dir(DEVICE_NAME, NULL);
	if (!hdmitx_dbgfs) {
		HDMITX_ERROR("can't create %s debugfs dir\n", DEVICE_NAME);
		return 0;
	}
	entry = debugfs_create_file("hdcp_log", S_IFREG | 0444,
			hdmitx_dbgfs, NULL,
			&hdcplog_ops);
	if (!entry)
		HDMITX_ERROR("debugfs create file %s failed\n", "hdcp_log");
	return 0;
}

void hdmitx_hdcp_exit(struct hdmitx_dev *hdev)
{
	if (hdev) {
		kthread_stop(hdev->task_hdcp);
		cancel_delayed_work_sync(&hdev->work_do_hdcp);
		kfree(hdev->topo_info);
		hdev->topo_info = NULL;
	}
}

MODULE_PARM_DESC(hdmi_authenticated, "\n hdmi_authenticated\n");
module_param(hdmi_authenticated, int, 0444);

