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

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of_device.h>
#include <linux/kthread.h>
#include <linux/freezer.h>

#include <uapi/linux/sched/types.h>

#include <media/dvbdev.h>
#include <media/dvb_frontend.h>

#include "mdrv_adapter.h"
#include "mdrv_adapter_dbg.h"
#include "mdrv_usbcam_stub.h"
#include "mdrv_cifpga.h"

#ifdef pr_fmt
#undef pr_fmt
#endif
#define pr_fmt(fmt) "mtk-dvb-adap: " fmt

#define MODEL_NAME			"mediatek,DVB"
#define DRV_NAME			"mediatek,dvb-adapter"

#define ADAPTER_ERR(fmt, ...)		\
	pr_err("[%s][%d]" fmt,	\
	__func__, __LINE__, ##__VA_ARGS__)

#define ADAPTER_INFO(fmt, ...)		\
	pr_info("[%s][%d]" fmt,	\
	__func__, __LINE__, ##__VA_ARGS__)

#define ADAPTER_DEBUG(fmt, ...)		\
	pr_debug("[%s][%d]" fmt,	\
	__func__, __LINE__, ##__VA_ARGS__)

#if defined(CONFIG_MEDIA_CONTROLLER_DVB) || defined(CONFIG_MTK_MEDIA_CONTROLLER_DVB)
#define has_op(notifier, op)	\
	((notifier) && (notifier)->op)

#define call_op(notifier, op, args...)	\
	(has_op((notifier), op) ? (notifier)->op(args) : 0)

#define SOURCE_PAD_IDX	0
#define SINK_PAD_IDX	1
#define FE_MAX			4
#define CA_MAX			8
#define TS_SYNC			(0x47)
#define USB_MAX_DATA_SIZE (0x100000)

typedef bool (*dvb_cmp_func_t)(struct media_entity *);

typedef enum {
	DVBDEV_HASH_TYPE_NONE,
	DVBDEV_HASH_TYPE_DEMOD,
	DVBDEV_HASH_TYPE_DEMUX,
	DVBDEV_HASH_TYPE_CI,
	DVBDEV_HASH_TYPE_CI2,
	DVBDEV_HASH_TYPE_CA,
	DVBDEV_HASH_TYPE_CIFPGA,
	DVBDEV_HASH_TYPE_MAX
} dvbdev_hash_type_t;

typedef struct {
	struct list_head list;
	struct dvb_device *dev;
	struct media_entity *entity;
	struct dvbdev_notifier notifier;
} dvbdev_hash_node_t;

struct dvbdev_fe_event_args {
	struct dvb_frontend *fe;
	enum cfm_notify_event_type_t ev_type;
	struct cfm_properties props;
	void *notify_data;
};

struct dvbdev_fe_event {
	bool locked;
	struct dvbdev_fe_event_args status;
	struct dvbdev_fe_event_args config;
};

struct ci2_task_args {
	struct media_entity *ts;
	struct media_entity *cam;
};

struct ci2_usb_fragment_h {
	u8 protocol_version;
	u8 LTS_id;
	u8 track_id;
	u8 reserved_future_use:5;
	u8 last_fragment:1;
	u8 first_fragment:1;
	u8 flush:1;
	u32 number_subsamples;
	u16 descriptor_lenth;
} __packed;
#endif

#define TH_READ_LOCK(__LOCK) \
	do { \
		read_lock(__LOCK); \
		while (atomic_read(__LOCK##_wr_cnt) != 0) { \
			read_unlock(__LOCK); \
			usleep_range(10, 20); \
			read_lock(__LOCK); \
		} \
		atomic_inc(__LOCK##_rd_cnt); \
		read_unlock(__LOCK); \
	} while (0)

#define TH_READ_UNLOCK(__LOCK) \
	atomic_dec(__LOCK##_rd_cnt)

#define TH_WRITE_LOCK(__LOCK) \
	do { \
		unsigned long flags; \
		write_lock_irqsave(__LOCK, flags); \
		atomic_inc(__LOCK##_wr_cnt); \
		write_unlock_irqrestore(__LOCK, flags); \
		while (atomic_read(__LOCK##_rd_cnt) != 0) \
			usleep_range(10, 20);\
	} while (0)

#define TH_WRITE_UNLOCK(__LOCK) \
	atomic_dec(__LOCK##_wr_cnt)

struct mdrv_dvb_adapter {
	struct platform_device *pdev;
	struct dvb_adapter adapter;
#if defined(CONFIG_MEDIA_CONTROLLER_DVB) || defined(CONFIG_MTK_MEDIA_CONTROLLER_DVB)
	struct mutex hash_mutex;
	struct list_head hash_list[DVBDEV_HASH_TYPE_MAX];
	struct media_device mdev;
	struct dvbdev_fe_event *fe_list;
	struct task_struct *task_ts2cam, *task_cam2ts, *task_cam2null;
	struct ci2_task_args data_ts2cam, data_cam2ts;
	struct ci2_usb_fragment_h fragment_h;
	bool cam_connected, cam_inserted;
	rwlock_t th_lock;
	atomic_t th_lock_wr_cnt;
	atomic_t th_lock_rd_cnt;
#endif
};

DVB_DEFINE_MOD_OPT_ADAPTER_NR(adapter_nr);

static ssize_t adap_sel_nr_show(struct device *dev,
	struct device_attribute *attr, char *buf)
{
	struct platform_device *pdev = to_platform_device(dev);
	struct mdrv_dvb_adapter *mdrv_adapter = platform_get_drvdata(pdev);
	int len;

	len = scnprintf(buf, PAGE_SIZE, "%u\n", mdrv_adapter->adapter.num);

	return len;
}

static bool usbcam_stub_en;
static bool data_ratio_monitor_en;
static bool ci2_p_i_en;
static bool cifpga_en;
static struct mutex notify_mutex;

struct file *file;

static u32 u32Ts2camDataCnt;
static u32 u32Cam2tsDataCnt;
static u32 u32Ts2camStartTime;
static u32 u32Cam2tsStartTime;

u32 _ci2_get_current_time(void)
{
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 15, 0)
	struct timespec64 ts;

	ktime_get_raw_ts64(&ts);
#else
	struct timespec ts;

	getrawmonotonic(&ts);
#endif
	return ts.tv_sec * time_1000 + ts.tv_nsec/time_1000000;
}

void _ci2_monitor_data_ratio(bool bTs2cam)
{
	u32 u32StartTime = bTs2cam ? u32Ts2camStartTime : u32Cam2tsStartTime;
	u32 u32Size = bTs2cam ? u32Ts2camDataCnt : u32Cam2tsDataCnt;
	u32 u32DiffTime = _ci2_get_current_time() - u32StartTime;

	if (u32DiffTime >= time_1000) {
		if (u32Size >= ONE_MB)
			ADAPTER_INFO("[%s] data ratio %uMB/s\n",
				bTs2cam ? "ts2cam":"cam2ts", u32Size/ONE_MB);
		else if (u32Size >= ONE_KB && u32Size < ONE_MB)
			ADAPTER_INFO("[%s] data ratio %uKB/s\n",
				bTs2cam  ? "ts2cam":"cam2ts", u32Size/ONE_KB);
		else
			ADAPTER_INFO("[%s] data ratio %uBytes/s\n",
				bTs2cam ? "ts2cam":"cam2ts", u32Size);

		if (bTs2cam) {
			u32Ts2camDataCnt = 0;
			u32Ts2camStartTime = _ci2_get_current_time();
		} else {
			u32Cam2tsDataCnt = 0;
			u32Cam2tsStartTime = _ci2_get_current_time();
		}
	}
}

static ssize_t usbcam_stub_show(struct device *dev,
	struct device_attribute *attr, char *buf)
{
	return scnprintf(buf, PAGE_SIZE, "%s\n", (usbcam_stub_en) ? "on" : "off");
}

static ssize_t usbcam_stub_store(struct device *dev,
	struct device_attribute *attr, const char *buf, size_t count)
{
	u32 value;

	if (kstrtouint(((buf != NULL) ? buf : ""), 0, &value) != 0)
		return -EINVAL;
	if ((value == 0) && (usbcam_stub_en == true)) {
		mdrv_usbcam_stub_exit();
		usbcam_stub_en = false;
	} else if ((value == 1) && (usbcam_stub_en == false)) {
		mdrv_usbcam_stub_init();
		usbcam_stub_en = true;
	}
	return count;
}

static ssize_t data_ratio_monitor_show(struct device *dev,
	struct device_attribute *attr, char *buf)
{
	return scnprintf(buf, PAGE_SIZE, "%s\n", (data_ratio_monitor_en) ? "on" : "off");
}

static ssize_t data_ratio_monitor_store(struct device *dev,
	struct device_attribute *attr, const char *buf, size_t count)
{
	u32 value;

	if (kstrtouint(((buf != NULL) ? buf : ""), 0, &value) != 0)
		return -EINVAL;

	u32Ts2camDataCnt = u32Cam2tsDataCnt = 0;
	u32Ts2camStartTime = u32Cam2tsStartTime = 0;
	if (value == 0)
		data_ratio_monitor_en = false;
	else if (value == 1) {
		data_ratio_monitor_en = true;
		u32Cam2tsStartTime = u32Ts2camStartTime = _ci2_get_current_time();
	}
	return count;
}

static ssize_t ci2_p_i_store(struct device *dev,
	struct device_attribute *attr, const char *buf, size_t count)
{
	u32 value;

	if (kstrtouint(((buf != NULL) ? buf : ""), 0, &value) != 0)
		return -EINVAL;
	if ((value == 0) && (ci2_p_i_en == true))
		ci2_p_i_en = false;
	else if ((value == 1) && (ci2_p_i_en == false))
		ci2_p_i_en = true;
	return count;
}

static ssize_t cifpga_show(struct device *dev,
	struct device_attribute *attr, char *buf)
{
	return scnprintf(buf, PAGE_SIZE, "%s\n", (cifpga_en) ? "on" : "off");
}

static ssize_t cifpga_store(struct device *dev,
	struct device_attribute *attr, const char *buf, size_t count)
{
	u32 value;

	if (kstrtouint(((buf != NULL) ? buf : ""), 0, &value) != 0)
		return -EINVAL;
	if ((value == 0) && (cifpga_en == true)) {
		mdrv_cifpga_exit();
		cifpga_en = false;
	} else if ((value == 1) && (cifpga_en == false)) {
		mdrv_cifpga_init();
		cifpga_en = true;
	}
	return count;
}

DEVICE_ATTR_RO(adap_sel_nr);
DEVICE_ATTR_RW(data_ratio_monitor);
DEVICE_ATTR_RW(usbcam_stub);
DEVICE_ATTR_WO(ci2_p_i);
DEVICE_ATTR_RW(cifpga);

static struct attribute *dvb_adapter_attrs[] = {
	&dev_attr_adap_sel_nr.attr,
	&dev_attr_usbcam_stub.attr,
	&dev_attr_data_ratio_monitor.attr,
	&dev_attr_ci2_p_i.attr,
	&dev_attr_cifpga.attr,
	NULL
};

static struct attribute_group dvb_adapter_attrs_group = {
	.name = "mtk_dbg",
	.attrs = dvb_adapter_attrs,
};

static struct mdrv_dvb_adapter (*_mdrv_adapter);
static dvbdev_hash_node_t *_get_hash_list_node(struct media_entity *entity);

#define CI2_RETRY			(4)
#define TS2CAM_INVL_US		(50 * 1000)
#define TIMES_INVL			(25)
#define CAM2TS_INVL_US		(TS2CAM_INVL_US / TIMES_INVL)
#define ACK_INTV_US			(10)
#define USLEEP_TIMEOUT		(100)

#define CHECK_PACKET_SIZE(a, b) ({ \
	bool ret = false; \
	if ((a >= USB_MAX_DATA_SIZE) || (b >= USB_MAX_DATA_SIZE)) \
		ret = true; \
	ret; \
})

#define UPDATE_CAM(__CAM) ({ \
	bool ret = false; \
	int i; \
	struct dvb_device *dvbdev; \
	for (i = 0; i < CA_MAX; i++) { \
		dvbdev = mdrv_adapter_find_device(DVB_DEVICE_CA, i); \
		if (dvbdev && is_ci2_device(dvbdev->entity)) { \
			__CAM = _get_hash_list_node(dvbdev->entity); \
			ret = true; \
			break; \
		} \
	} \
	ret; \
})

bool _ci2_ts2cam_check_data_ready_notify(int i, int retry)
{
	if (likely(data_ratio_monitor_en))
		_ci2_monitor_data_ratio(true);

	if (i == 0)
		return true;
	if (retry == CI2_RETRY) {
		ADAPTER_ERR("data_ready_notify fail!\n");
		return true;
	}
	return false;
}

void _ci2_cam2ts_file_operator(enum file_operator mode, void *buf, u32 len)
{
	if (ci2_p_i_en) {
		switch (mode) {
		case FILE_OPEN:
			file = mdrv_adapter_file_open("/data/ci2-input.ts",
				O_RDWR|O_CREAT, 0644);
			if (!file)
				ADAPTER_ERR("cannot open /data/ci2-input.ts\n");
			break;
		case FILE_WRITE:
			if (file)
				mdrv_adapter_file_write(file, buf, len);
			break;
		case FILE_CLOSE:
			if (file) {
				mdrv_adapter_file_close(file);
				file = NULL;
			}
			break;
		default:
			ADAPTER_ERR("invalid file operator type\n");
			break;
		}
	}
}

static int _ci2_task_cam2null(void *data)
{
	int i;
	dvbdev_hash_node_t *cam;
	struct dvbdev_properties p;
	struct dvbdev_property prop[2];

	while (!kthread_should_stop()) {
		set_current_state(TASK_INTERRUPTIBLE);
		schedule();
		set_current_state(TASK_RUNNING);

		TH_READ_LOCK(&_mdrv_adapter->th_lock);

		if (_mdrv_adapter->cam_inserted == false) {
			TH_READ_UNLOCK(&_mdrv_adapter->th_lock);
			continue;
		}
		cam = NULL;
		if (UPDATE_CAM(cam)) {
			for (i = 0; i < TIMES_INVL; i++) {
				memset(&prop, 0, sizeof(prop));
				prop[0].cmd = ADAP_CMD_CI2_BUFFER_INFO;
				prop[1].cmd = ADAP_CMD_CI2_BUFFER_INFO;
				p.num = 2;
				p.props = prop;
				call_op(&cam->notifier, query_notify, cam->dev, &p);
				usleep_range(CAM2TS_INVL_US, (CAM2TS_INVL_US + USLEEP_TIMEOUT));
				if (_mdrv_adapter->cam_connected == true)
					break;
			}
		}
		TH_READ_UNLOCK(&_mdrv_adapter->th_lock);
	}
	return 0;
}

static int _ci2_task_ts2cam(void *data)
{
	struct ci2_task_args *d = (struct ci2_task_args *)data;
	dvbdev_hash_node_t *ts = _get_hash_list_node(d->ts);
	dvbdev_hash_node_t *cam = _get_hash_list_node(d->cam);
	struct dvbdev_properties p;
	struct dvbdev_property prop[2];
	int ret, retry = 0, i, ack_cnt = 0;
	u64 *wp0 = NULL, *wp1 = NULL;
	bool reconnect = false;

	ADAPTER_INFO("start ts2cam monitor!\n");
	set_freezable();
	wp0 = kzalloc(sizeof(u64), GFP_KERNEL);
	wp1 = kzalloc(sizeof(u64), GFP_KERNEL);
	memset(&prop, 0, sizeof(prop));
	prop[0].cmd = ADAP_CMD_CI2_STREAMING_STATUS;
	prop[0].data[0] = ADAP_CI2_STREAMING_ON;
	prop[0].data[1] = ADAP_CI2_STREAMING_SOURCE;
	p.num = 1;
	p.props = prop;
	ret = call_op(&ts->notifier, query_notify, ts->dev, &p);
	if (ret < 0)
		ADAPTER_ERR("fail to set streaming status!\n");
	while (!kthread_should_stop()) {
		try_to_freeze();
		usleep_range(TS2CAM_INVL_US, (TS2CAM_INVL_US + USLEEP_TIMEOUT));

		TH_READ_LOCK(&_mdrv_adapter->th_lock);
		if (_mdrv_adapter->cam_inserted == false) {
			reconnect = true;
			goto LOOP;
		}
		if (likely(!usbcam_stub_en)) {
			if (reconnect) {
				if (UPDATE_CAM(cam)) {
					ADAPTER_INFO("update cam info (ts2cam)\n");
					reconnect = false;
				} else
					goto LOOP;
			}
		}

		memset(&prop, 0, sizeof(prop));
		prop[0].cmd = ADAP_CMD_CI2_BUFFER_INFO;
		prop[0].buffer.buf = wp0;
		prop[1].cmd = ADAP_CMD_CI2_BUFFER_INFO;
		prop[1].buffer.buf = wp1;
		p.num = 2;
		p.props = prop;
		ret = call_op(&ts->notifier, query_notify, ts->dev, &p);
		if ((ret < 0) || CHECK_PACKET_SIZE(prop[0].buffer.len, prop[1].buffer.len))
			goto LOOP;

		ADAPTER_DEBUG("data ready! buf0@ 0x%llx len: 0x%x\n",
			(u64)(size_t)(prop[0].buffer.buf), prop[0].buffer.len);
		ADAPTER_DEBUG("data ready! buf1@ 0x%llx len: 0x%x\n",
			(u64)(size_t)(prop[1].buffer.buf), prop[1].buffer.len);

		i = retry = 0;
		while (i < p.num && retry < CI2_RETRY &&
			p.props[i].buffer.len != 0) {
			if (likely(data_ratio_monitor_en))
				u32Ts2camDataCnt += p.props[i].buffer.len;
			ret = call_op(&cam->notifier, data_ready_notify,
				cam->dev, &(_mdrv_adapter->fragment_h),
				sizeof(struct ci2_usb_fragment_h), NULL);
			ret = call_op(&cam->notifier, data_ready_notify,
				cam->dev, p.props[i].buffer.buf,
				p.props[i].buffer.len, NULL);
			if (ret < 0)
				retry++;
			else
				i++;
		}
		if (_ci2_ts2cam_check_data_ready_notify(i, retry))
			goto LOOP;

		memset(&prop, 0, sizeof(prop));
		prop[0].cmd = ADAP_CMD_CI2_BUFFER_ACK;
		p.num = 1;
		p.props = prop;
WAIT_ACK:
		ret = call_op(&cam->notifier, query_notify, cam->dev, &p);
		if (p.props[0].value == 0 || ret < 0) {
			usleep_range(ACK_INTV_US, (ACK_INTV_US + USLEEP_TIMEOUT));
			ack_cnt++;
			if (ack_cnt < 10)
				goto WAIT_ACK;
			else
				ADAPTER_ERR("no cam ack, continue!\n");
		}
		ack_cnt = 0;

		if (_mdrv_adapter->cam_connected == false)
			wake_up_process(_mdrv_adapter->task_cam2null);
LOOP:
		TH_READ_UNLOCK(&_mdrv_adapter->th_lock);
	}
	ADAPTER_INFO("stop ts2cam monitor!\n");
	u32Ts2camDataCnt = u32Ts2camStartTime = 0;
	memset(&prop, 0, sizeof(prop));
	prop[0].cmd = ADAP_CMD_CI2_STREAMING_STATUS;
	prop[0].data[0] = ADAP_CI2_STREAMING_OFF;
	prop[0].data[1] = ADAP_CI2_STREAMING_SOURCE;
	p.num = 1;
	p.props = prop;
	ret = call_op(&ts->notifier, query_notify, ts->dev, &p);
	if (ret < 0)
		ADAPTER_ERR("fail to set streaming status!\n");
	kfree(wp0);
	kfree(wp1);
	return 0;
}

static int _ci2_task_cam2ts(void *data)
{
	struct ci2_task_args *d = (struct ci2_task_args *)data;
	dvbdev_hash_node_t *ts = _get_hash_list_node(d->ts);
	dvbdev_hash_node_t *cam = _get_hash_list_node(d->cam);
	struct dvbdev_properties p;
	struct dvbdev_property prop[2];
	int ret, i;
	bool reconnect = false;

	ADAPTER_INFO("start cam2ts monitor!\n");
	set_freezable();
	memset(&prop, 0, sizeof(prop));
	prop[0].cmd = ADAP_CMD_CI2_STREAMING_STATUS;
	prop[0].data[0] = ADAP_CI2_STREAMING_ON;
	prop[0].data[1] = ADAP_CI2_STREAMING_SINK;
	p.num = 1;
	p.props = prop;
	ret = call_op(&ts->notifier, query_notify, ts->dev, &p);
	if (ret < 0)
		ADAPTER_ERR("fail to set streaming status!\n");
	_ci2_cam2ts_file_operator(FILE_OPEN, NULL, 0);
	while (!kthread_should_stop()) {
		try_to_freeze();
		usleep_range(CAM2TS_INVL_US, (CAM2TS_INVL_US + USLEEP_TIMEOUT));

		TH_READ_LOCK(&_mdrv_adapter->th_lock);
		if (_mdrv_adapter->cam_inserted == false) {
			reconnect = true;
			goto LOOP;
		}
		if (likely(!usbcam_stub_en)) {
			if (reconnect) {
				if (UPDATE_CAM(cam)) {
					ADAPTER_INFO("update cam info (cam2ts)\n");
					reconnect = false;
				} else
					goto LOOP;
			}
		}

		memset(&prop, 0, sizeof(prop));
		prop[0].cmd = ADAP_CMD_CI2_BUFFER_INFO;
		prop[1].cmd = ADAP_CMD_CI2_BUFFER_INFO;
		p.num = 2;
		p.props = prop;
		ret = call_op(&cam->notifier, query_notify, cam->dev, &p);
		if (ret == -ENODEV)
			goto LOOP;

		for (i = 0; i < p.num; i++) {
			if (p.props[i].buffer.len <= sizeof(struct ci2_usb_fragment_h) &&
				(p.props[i].buffer.buf != NULL) &&
				(*(u8 *)(p.props[i].buffer.buf) != TS_SYNC))
				continue;
			if (likely(data_ratio_monitor_en) && (p.props[i].buffer.len != 0))
				u32Cam2tsDataCnt += p.props[i].buffer.len;
			_ci2_cam2ts_file_operator(FILE_WRITE, p.props[i].buffer.buf,
				p.props[i].buffer.len);
			ret = call_op(&ts->notifier, data_ready_notify,
				ts->dev, p.props[i].buffer.buf,
				p.props[i].buffer.len, NULL);
		}
		if (likely(data_ratio_monitor_en))
			_ci2_monitor_data_ratio(false);
LOOP:
		TH_READ_UNLOCK(&_mdrv_adapter->th_lock);
	}
	_ci2_cam2ts_file_operator(FILE_CLOSE, NULL, 0);
	ADAPTER_INFO("stop cam2ts monitor!\n");
	u32Cam2tsDataCnt = u32Cam2tsStartTime = 0;
	memset(&prop, 0, sizeof(prop));
	prop[0].cmd = ADAP_CMD_CI2_STREAMING_STATUS;
	prop[0].data[0] = ADAP_CI2_STREAMING_OFF;
	prop[0].data[1] = ADAP_CI2_STREAMING_SINK;
	p.num = 1;
	p.props = prop;
	ret = call_op(&ts->notifier, query_notify, ts->dev, &p);
	if (ret < 0)
		ADAPTER_ERR("fail to set streaming status!\n");
	return 0;
}

static void __cfm_event_notify(
	struct dvb_frontend *fe,
	enum cfm_notify_event_type_t ev_type,
	struct cfm_properties *props,
	void *notify_data);

#if defined(CONFIG_MEDIA_CONTROLLER_DVB) || defined(CONFIG_MTK_MEDIA_CONTROLLER_DVB)
static int _link_notify(
	const struct media_pad *pad,
	link_state_t state,
	enum link_end end,
	unsigned int notification)
{
	int ret = 0;
	struct dvbdev_link link;
	dvbdev_hash_node_t *node;

	node = _get_hash_list_node(pad->entity);

	if (node) {
		link.state = state;
		link.end = end;
		if (notification == MEDIA_DEV_NOTIFY_PRE_LINK_CH)
			ret = call_op(&node->notifier, prelink_notify, node->dev, &link);
		else
			ret = call_op(&node->notifier, postlink_notify, node->dev, &link);

		if (ret < 0) {
			ADAPTER_ERR("call_op: (%s) %s fail\n",
				pad->entity->name,
				(notification == MEDIA_DEV_NOTIFY_PRE_LINK_CH) ?
						"prelink_notify" :
						"postlink_notify");
			return -EFAULT;
		}
	}

	return ret;
}

static int _link_notify_prelink(
	struct media_link *link,
	unsigned int notification)
{
	int ret = 0;
	struct media_pad *source_pad;
	struct media_pad *sink_pad;
	struct media_entity *source;
	struct media_entity *sink;
	link_state_t link_op = LINK_STATE_DISCONNECT;

	if (link == NULL)
		return -EINVAL;
	source_pad = link->source;
	sink_pad = link->sink;
	if ((source_pad == NULL) || (sink_pad == NULL))
		return -EINVAL;
	source = source_pad->entity;
	sink = sink_pad->entity;
	if ((source == NULL) || (sink == NULL))
		return -EINVAL;
	if (!(link->flags & MEDIA_LNK_FL_ENABLED))
		link_op = LINK_STATE_CONNECT;

	ADAPTER_INFO("[prelink: previous flags = %lx] source(%s,%u,%u) --> sink(%s,%u,%u)\n",
		link->flags,
		source->name, source->graph_obj.id, source_pad->index,
		sink->name, sink->graph_obj.id, sink_pad->index);

	if (_link_notify(
		source_pad,
		link_op,
		LINK_END_SOURCE,
		notification) != 0)
		ADAPTER_ERR("PRE_LINK_CH source fail! link op: %s\n",
				(link_op == LINK_STATE_CONNECT) ?
				"LINK_STATE_CONNECT" : "LINK_STATE_DISCONNECT");
	if (_link_notify(
		sink_pad,
		link_op,
		LINK_END_SINK,
		notification) != 0)
		ADAPTER_ERR("PRE_LINK_CH sink fail! link op: %s\n",
				(link_op == LINK_STATE_CONNECT) ?
				"LINK_STATE_CONNECT" : "LINK_STATE_DISCONNECT");

	if (link->flags & MEDIA_LNK_FL_ENABLED) {
		if (is_ts_device(source) && is_ci2_device(sink)) {
			if (_mdrv_adapter->task_ts2cam) {
				ret = kthread_stop(_mdrv_adapter->task_ts2cam);
				if (ret < 0)
					ADAPTER_ERR("stop thread fail!\n");
				else
					_mdrv_adapter->task_ts2cam = NULL;
			}
		} else if (is_ci2_device(source) && is_ts_device(sink)) {
			_mdrv_adapter->cam_connected = false;
			if (_mdrv_adapter->task_cam2ts) {
				ret = kthread_stop(_mdrv_adapter->task_cam2ts);
				if (ret < 0)
					ADAPTER_ERR("stop thread fail!\n");
				else
					_mdrv_adapter->task_cam2ts  = NULL;
			}
		}
	}

	return ret;
}

static int _link_notify_postlink_ts_2_ci2(
		struct media_link *link,
		unsigned int notification)
{
#define SCHED_PRIORITY (40)

	int ret = 0;
	struct media_pad *source_pad;
	struct media_pad *sink_pad;
	struct media_entity *source;
	struct media_entity *sink;
	struct sched_param stThrSched = { 0 };

	if (link == NULL)
		return -EINVAL;
	source_pad = link->source;
	sink_pad = link->sink;
	if ((source_pad == NULL) || (sink_pad == NULL))
		return -EINVAL;
	source = source_pad->entity;
	sink = sink_pad->entity;
	if ((source == NULL) || (sink == NULL))
		return -EINVAL;

	stThrSched.sched_priority = SCHED_PRIORITY;

	if (_mdrv_adapter->task_ts2cam) {
		ret = kthread_stop(_mdrv_adapter->task_ts2cam);
		if (ret < 0)
			ADAPTER_ERR("stop thread fail!\n");
		else
			_mdrv_adapter->task_ts2cam = NULL;
	}
	/*
	 * stop if leakage occurred.
	 */
	if (_mdrv_adapter->task_ts2cam == NULL) {
		_mdrv_adapter->data_ts2cam.ts = source;
		_mdrv_adapter->data_ts2cam.cam = sink;
		_mdrv_adapter->task_ts2cam = kthread_run(_ci2_task_ts2cam,
			&(_mdrv_adapter->data_ts2cam), "dvbci_ts2cam");
		sched_setscheduler_nocheck(_mdrv_adapter->task_ts2cam,
			SCHED_RR, &stThrSched);
	}

	return ret;

#undef SCHED_PRIORITY
}

static int _link_notify_postlink_ci2_2_ts(
		struct media_link *link,
		unsigned int notification)
{
#define SCHED_PRIORITY (40)

	int ret = 0;
	struct media_pad *source_pad;
	struct media_pad *sink_pad;
	struct media_entity *source;
	struct media_entity *sink;
	struct sched_param stThrSched = { 0 };

	if (link == NULL)
		return -EINVAL;
	source_pad = link->source;
	sink_pad = link->sink;
	if ((source_pad == NULL) || (sink_pad == NULL))
		return -EINVAL;
	source = source_pad->entity;
	sink = sink_pad->entity;
	if ((source == NULL) || (sink == NULL))
		return -EINVAL;

	stThrSched.sched_priority = SCHED_PRIORITY;

	_mdrv_adapter->cam_connected = true;
	if (_mdrv_adapter->task_cam2ts) {
		ret = kthread_stop(_mdrv_adapter->task_cam2ts);
		if (ret < 0)
			ADAPTER_ERR("stop thread fail!\n");
		else
			_mdrv_adapter->task_cam2ts = NULL;
	}
	/*
	 * stop if leakage occurred.
	 */
	if (_mdrv_adapter->task_cam2ts == NULL) {
		_mdrv_adapter->data_cam2ts.ts = sink;
		_mdrv_adapter->data_cam2ts.cam = source;
		_mdrv_adapter->task_cam2ts = kthread_run(_ci2_task_cam2ts,
			&(_mdrv_adapter->data_cam2ts), "dvbci_cam2ts");
		sched_setscheduler_nocheck(_mdrv_adapter->task_cam2ts,
			SCHED_RR, &stThrSched);
	}

	return ret;

#undef SCHED_PRIORITY
}

static int _link_notify_postlink(
	struct media_link *link,
	unsigned int notification)
{
	int ret = 0;
	struct media_pad *source_pad;
	struct media_pad *sink_pad;
	struct media_entity *source;
	struct media_entity *sink;
	int i;
	link_state_t link_op = LINK_STATE_CONNECT;
	struct dvbdev_fe_event *p = NULL;

	if (link == NULL)
		return -EINVAL;
	source_pad = link->source;
	sink_pad = link->sink;
	if ((source_pad == NULL) || (sink_pad == NULL))
		return -EINVAL;
	source = source_pad->entity;
	sink = sink_pad->entity;
	if ((source == NULL) || (sink == NULL))
		return -EINVAL;
	if (!(link->flags & MEDIA_LNK_FL_ENABLED))
		link_op = LINK_STATE_DISCONNECT;

	ADAPTER_INFO("[postlink: current flags = %lx] source(%s,%u,%u) --> sink(%s,%u,%u)\n",
		link->flags,
		source->name, source->graph_obj.id, source_pad->index,
		sink->name, sink->graph_obj.id, sink_pad->index);

	if (link->flags & MEDIA_LNK_FL_ENABLED) {
		if (is_ts_device(source) && is_ci2_device(sink))
			ret = _link_notify_postlink_ts_2_ci2(link, notification);
		else if (is_ci2_device(source) && is_ts_device(sink))
			ret = _link_notify_postlink_ci2_2_ts(link, notification);
	}

	if (_link_notify(
		source_pad,
		link_op,
		LINK_END_SOURCE,
		notification) != 0)
		ADAPTER_ERR("POST_LINK_CH source fail! link op: %s\n",
				(link_op == LINK_STATE_CONNECT) ?
				"LINK_STATE_CONNECT" : "LINK_STATE_DISCONNECT");
	if (_link_notify(
		sink_pad,
		link_op,
		LINK_END_SINK,
		notification) != 0)
		ADAPTER_ERR("POST_LINK_CH sink fail! link op: %s\n",
				(link_op == LINK_STATE_CONNECT) ?
				"LINK_STATE_CONNECT" : "LINK_STATE_DISCONNECT");

	if (link->flags & MEDIA_LNK_FL_ENABLED) {
		for (i = 0; i < FE_MAX; i++) {
			mutex_lock(&notify_mutex);
			p = &_mdrv_adapter->fe_list[i];
			if (p->locked) {
				ADAPTER_DEBUG("fe: %d is locked before media links are set\n", i);
				__cfm_event_notify(p->status.fe, p->status.ev_type,
					&p->status.props, p->status.notify_data);
				__cfm_event_notify(p->config.fe, p->config.ev_type,
					&p->config.props, p->config.notify_data);
			}
			mutex_unlock(&notify_mutex);
		}
	}

	return ret;
}

static int _mdev_link_notify(
	struct media_link *link,
	unsigned int flags,
	unsigned int notification)
{
	int ret = 0;

	if (notification == MEDIA_DEV_NOTIFY_PRE_LINK_CH)
		ret = _link_notify_prelink(link, notification);
	else if (notification == MEDIA_DEV_NOTIFY_POST_LINK_CH)
		ret = _link_notify_postlink(link, notification);

	return ret;
}

static const struct media_device_ops mdev_ops = {
	.link_notify = _mdev_link_notify,
};

static int _create_links(
	struct media_entity *source,
	const u32 source_function,
	struct media_entity *sink,
	const u32 sink_function,
	dvb_cmp_func_t fn)
{
	int ret;
	struct media_device *mdev;
	struct media_entity *entity;
	struct media_pad *source_pad, *sink_pad;
	u32 function;

	/* n:n releation */
	if (!source && !sink)
		return -EINVAL;

	/* 1:1 relation */
	if (source && sink) {
		source_pad = &source->pads[SOURCE_PAD_IDX];
		sink_pad = &sink->pads[SINK_PAD_IDX];

		if (!media_entity_find_link(source_pad, sink_pad)) {
			source->pads[SOURCE_PAD_IDX].flags = MEDIA_PAD_FL_SOURCE;
			sink->pads[SINK_PAD_IDX].flags = MEDIA_PAD_FL_SINK;
			return media_create_pad_link(
				source, SOURCE_PAD_IDX, sink, SINK_PAD_IDX, 0);
		}
	}

	/* 1:n or n:1 relation */
	if (source) {
		mdev = source->graph_obj.mdev;
		function = sink_function;
	} else {
		mdev = sink->graph_obj.mdev;
		function = source_function;
	}

	media_device_for_each_entity(entity, mdev) {
		if (entity->function != function)
			continue;

		if (fn && !fn(entity))
			continue;

		ret = 0;

		if (source) {
			source_pad = &source->pads[SOURCE_PAD_IDX];
			sink_pad = &entity->pads[SINK_PAD_IDX];

			if (!media_entity_find_link(source_pad, sink_pad)) {
				source->pads[SOURCE_PAD_IDX].flags = MEDIA_PAD_FL_SOURCE;
				entity->pads[SINK_PAD_IDX].flags = MEDIA_PAD_FL_SINK;
				ret = media_create_pad_link(source, SOURCE_PAD_IDX,
					entity, SINK_PAD_IDX, 0);
			}
		} else {
			source_pad = &entity->pads[SOURCE_PAD_IDX];
			sink_pad = &sink->pads[SINK_PAD_IDX];

			if (!media_entity_find_link(source_pad, sink_pad)) {
				entity->pads[SOURCE_PAD_IDX].flags = MEDIA_PAD_FL_SOURCE;
				sink->pads[SINK_PAD_IDX].flags = MEDIA_PAD_FL_SINK;
				ret = media_create_pad_link(entity, SOURCE_PAD_IDX,
					sink, SINK_PAD_IDX, 0);
			}
		}

		if (ret)
			return ret;
	}

	return 0;
}

/*
 * dynamically create links:
 *      [Demod] -> [TSO] -> [CI] -> [Demux] -> [CA]
 *          |        |_______________^ | ^
 *          |________________________| | |
 *                                     | [CI2]
 *                                     | ^
 *                                     |_|
 */
static void _mdev_entity_notify(struct media_entity *entity, void *notify_data)
{
	switch (entity->function) {
	case MEDIA_ENT_F_DTV_DEMOD:
		_create_links(
			entity, MEDIA_ENT_F_DTV_DEMOD,
			NULL, MEDIA_ENT_F_TS_DEMUX,
			0);
		break;
	case MEDIA_ENT_F_TS_DEMUX:
		if (is_tso_device(entity)) {
			_create_links(
				NULL, MEDIA_ENT_F_DTV_DEMOD,
				entity, MEDIA_ENT_F_TS_DEMUX,
				0);
			_create_links(
				entity, MEDIA_ENT_F_TS_DEMUX,
				NULL, MEDIA_ENT_F_DTV_CA,
				&is_ci_device);
			_create_links(
				entity, MEDIA_ENT_F_TS_DEMUX,
				NULL, MEDIA_ENT_F_DTV_CA,
				&is_cifpga_device);
			_create_links(
				entity, MEDIA_ENT_F_TS_DEMUX,
				NULL, MEDIA_ENT_F_TS_DEMUX,
				&is_ts_device);
		} else {
			_create_links(
				NULL, MEDIA_ENT_F_DTV_DEMOD,
				entity, MEDIA_ENT_F_TS_DEMUX,
				0);
			_create_links(
				NULL, MEDIA_ENT_F_DTV_CA,
				entity, MEDIA_ENT_F_TS_DEMUX,
				&is_ci_device);
			_create_links(
				NULL, MEDIA_ENT_F_DTV_CA,
				entity, MEDIA_ENT_F_TS_DEMUX,
				&is_cifpga_device);
			_create_links(
				entity, MEDIA_ENT_F_TS_DEMUX,
				NULL, MEDIA_ENT_F_DTV_CA,
				&is_ca_device);
			_create_links(
				NULL, MEDIA_ENT_F_TS_DEMUX,
				entity, MEDIA_ENT_F_TS_DEMUX,
				&is_tso_device);
			_create_links(
				NULL, MEDIA_ENT_F_DTV_CA,
				entity, MEDIA_ENT_F_TS_DEMUX,
				&is_ci2_device);
			_create_links(
				entity, MEDIA_ENT_F_TS_DEMUX,
				NULL, MEDIA_ENT_F_DTV_CA,
				&is_ci2_device);
		}
		break;
	case MEDIA_ENT_F_DTV_CA:
		if (is_ci_device(entity)) {
			_create_links(
				entity, MEDIA_ENT_F_DTV_CA,
				NULL, MEDIA_ENT_F_TS_DEMUX,
				&is_ts_device);
			_create_links(
				NULL, MEDIA_ENT_F_TS_DEMUX,
				entity, MEDIA_ENT_F_DTV_CA,
				&is_tso_device);
		} else if (is_ci2_device(entity)) {
			_create_links(
				NULL, MEDIA_ENT_F_TS_DEMUX,
				entity, MEDIA_ENT_F_DTV_CA,
				&is_ts_device);
			_create_links(
				entity, MEDIA_ENT_F_DTV_CA,
				NULL, MEDIA_ENT_F_TS_DEMUX,
				&is_ts_device);
		} else if (is_ca_device(entity)) {
			_create_links(
				NULL, MEDIA_ENT_F_TS_DEMUX,
				entity, MEDIA_ENT_F_DTV_CA,
				&is_ts_device);
		} else if (is_cifpga_device(entity)) {
			_create_links(
				entity, MEDIA_ENT_F_DTV_CA,
				NULL, MEDIA_ENT_F_TS_DEMUX,
				&is_ts_device);
			_create_links(
				NULL, MEDIA_ENT_F_TS_DEMUX,
				entity, MEDIA_ENT_F_DTV_CA,
				&is_tso_device);
		}
		break;
	default:
		return;
	}

	ADAPTER_INFO("add new entity(%s , %d)\n",
		entity->name, entity->graph_obj.id);
}

static struct media_entity_notify entity_notify = {
	.list = LIST_HEAD_INIT(entity_notify.list),
	.notify = _mdev_entity_notify,
	.notify_data = NULL,
};

static int _init_mdev(struct media_device *mdev, struct dvb_adapter *adapter)
{
	strscpy(mdev->model, MODEL_NAME, sizeof(mdev->model));
	strscpy(mdev->driver_name, DRV_NAME, sizeof(mdev->driver_name));
	mdev->ops = &mdev_ops;
	mdev->dev = adapter->device;

	media_device_init(mdev);

	if (media_device_register(mdev) < 0) {
		ADAPTER_ERR("register media device fail\n");
		return -EFAULT;
	}

#if LINUX_VERSION_CODE < KERNEL_VERSION(6, 6, 0)
	if (media_device_register_entity_notify(mdev, &entity_notify) < 0) {
		ADAPTER_ERR("register media device fail\n");
		return -EFAULT;
	}
#else
	media_device_register_entity_notify(mdev, &entity_notify);
#endif

	dvb_register_media_controller(adapter, mdev);

	return 0;
}

static void _release_mdev(struct media_device *mdev)
{
	media_device_unregister(mdev);
}

static void _init_hash_list(struct mdrv_dvb_adapter *adap)
{
	int i;

	mutex_init(&adap->hash_mutex);

	for (i = 0; i < DVBDEV_HASH_TYPE_MAX; i++)
		INIT_LIST_HEAD(&adap->hash_list[i]);
}

static dvbdev_hash_type_t _get_hash_list_type(struct media_entity *entity)
{
	switch (entity->function) {
	case MEDIA_ENT_F_DTV_DEMOD:
		return DVBDEV_HASH_TYPE_DEMOD;
	case MEDIA_ENT_F_TS_DEMUX:
		return DVBDEV_HASH_TYPE_DEMUX;
	case MEDIA_ENT_F_DTV_CA:
		if (is_ci_device(entity))
			return DVBDEV_HASH_TYPE_CI;
		else if (is_ci2_device(entity))
			return DVBDEV_HASH_TYPE_CI2;
		else if (is_ca_device(entity))
			return DVBDEV_HASH_TYPE_CA;
		else if (is_cifpga_device(entity))
			return DVBDEV_HASH_TYPE_CIFPGA;
		else
			return DVBDEV_HASH_TYPE_NONE;
	default:
		return DVBDEV_HASH_TYPE_NONE;
	}
}

static dvbdev_hash_node_t *_get_hash_list_node(struct media_entity *entity)
{
	dvbdev_hash_type_t type = _get_hash_list_type(entity);
	dvbdev_hash_node_t *node, *next;

	if (type == DVBDEV_HASH_TYPE_NONE)
		return NULL;

	mutex_lock(&_mdrv_adapter->hash_mutex);

	list_for_each_entry_safe(node, next, &_mdrv_adapter->hash_list[type], list)
		if (node->entity == entity) {
			mutex_unlock(&_mdrv_adapter->hash_mutex);
			return node;
		}

	mutex_unlock(&_mdrv_adapter->hash_mutex);
	return NULL;
}

static int _mdev_entity_link_setup(
	struct media_entity *entity,
	const struct media_pad *local,
	const struct media_pad *remote,
	u32 flags)
{
	int ret = 0;
	struct dvbdev_link link;
	dvbdev_hash_node_t *local_node, *remote_node;

	local_node = _get_hash_list_node(local->entity);
	remote_node = _get_hash_list_node(remote->entity);

	if (local_node) {
		link.remote = (remote_node) ? remote_node->dev : NULL;
		link.remote_entity = remote->entity;
		link.state = (flags & MEDIA_LNK_FL_ENABLED) ?
			LINK_STATE_CONNECT : LINK_STATE_DISCONNECT;
		ret = call_op(&local_node->notifier, link_notify, local_node->dev, &link);
		if (ret < 0) {
			ADAPTER_ERR("call_op: (%s,%s) link_notify fail\n",
				local->entity->name, remote->entity->name);
			return -EFAULT;
		}
	}

	return ret;
}

static const struct media_entity_operations entify_ops = {
	.link_setup = _mdev_entity_link_setup,
};

static void _copy_cfm_props(struct cfm_properties *src, struct cfm_properties *dst)
{
	if (src == NULL) {
		ADAPTER_ERR("src is null\n");
		return;
	}
	if (dst == NULL) {
		ADAPTER_ERR("dst is null\n");
		return;
	}

	if (dst->props != NULL && src->num != dst->num) {
		kfree(dst->props);
		dst->props = NULL;
	}

	dst->num = 0;
	if (dst->props == NULL && src->num > 0) {
		dst->props = kcalloc(src->num, sizeof(struct cfm_property), GFP_KERNEL);
		if (dst->props == NULL) {
			ADAPTER_ERR("kcalloc for dst prop failed\n");
			return;
		}
	}

	if (dst->props) {
		dst->num = src->num;
		memcpy(dst->props, src->props, src->num*sizeof(struct cfm_property));
	}
}

static void _dump_cfm_props(struct cfm_properties *props)
{
	u32 i;

	ADAPTER_DEBUG("property number = %u, addr = %p\n", props->num, props);
	for (i = 0; i < props->num; i++) {
		ADAPTER_DEBUG("######## (idx: %u) ########\n", i);
		ADAPTER_DEBUG("cmd: %x\n", props->props[i].cmd);
		ADAPTER_DEBUG("data = %x\n", props->props[i].u.data);
	}
}

static inline struct dvb_device *_fe_to_dev(struct dvb_frontend *fe)
{
	if (!fe) {
		ADAPTER_ERR("fe == NULL\n");
		return NULL;
	}

	return mdrv_adapter_find_device(DVB_DEVICE_FRONTEND, fe->id);
}

static int entity2sink(struct media_entity *source,
	u32 type, dvbdev_hash_node_t *node)
{
	struct media_pad *source_pad, *sink_pad;
	struct media_entity *sink;
	struct media_link *link;
	dvbdev_hash_node_t *p;
	int i = 0;

	source_pad = &source->pads[SOURCE_PAD_IDX];
	media_device_for_each_entity(sink, source->graph_obj.mdev) {
		sink_pad = &sink->pads[SINK_PAD_IDX];
		link = media_entity_find_link(source_pad, sink_pad);
		if (link && (link->flags & MEDIA_LNK_FL_ENABLED)) {
			p = _get_hash_list_node(sink);
			if (p) {
				memcpy((node + i), p, sizeof(dvbdev_hash_node_t));
				i++;
			}
		}
	}
	return i;
}

static u32 recursive_cnt;
#define DEPTH_NOTIFY_NEXT (10)

struct notify_fe_data {
	struct dvb_frontend *fe;
	enum cfm_notify_event_type_t ev_type;
	struct cfm_properties *props;
};

static void notify_next(struct media_entity *source, struct notify_fe_data *data)
{
	int i, cnt, size, ret;
	dvbdev_hash_node_t *node = NULL;
	struct media_entity *next;

	if (WARN_ON(recursive_cnt >= DEPTH_NOTIFY_NEXT))
		return;
	recursive_cnt++;
	size = (source->num_links - source->num_backlinks) * (sizeof(dvbdev_hash_node_t));
	node = kzalloc(size, GFP_KERNEL);
	if (node) {
		cnt = entity2sink(source, 0, node);
		for (i = 0; i < cnt; i++) {
			ADAPTER_INFO("event_type = %d : source(%s,%d) - sink(%s,%d) (step:%d)\n",
				data->ev_type,
				source->name, source->graph_obj.id,
				node[i].entity->name, node[i].entity->graph_obj.id,
				recursive_cnt);
			ret = call_op(&node[i].notifier, streaming_event_notify,
				node[i].dev, data->ev_type, data->props, data->fe);
			if (ret < 0)
				ADAPTER_ERR("notify next fail\n");
			next = node[i].entity;
			notify_next(next, data);
		}
		kfree(node);
	}
	recursive_cnt--;
}

static void __cfm_event_notify(
	struct dvb_frontend *fe,
	enum cfm_notify_event_type_t ev_type,
	struct cfm_properties *props,
	void *notify_data)
{
	struct dvb_device *dvbdev;
	struct media_entity *source;
	struct notify_fe_data data;

	if (!fe) {
		ADAPTER_ERR("fe == NULL\n");
		return;
	}

	dvbdev = _fe_to_dev(fe);
	if (!dvbdev) {
		ADAPTER_ERR("can't find frontend (id: %d)\n", fe->id);
		return;
	}

	source = dvbdev->entity;
	data.fe = fe;
	data.ev_type = ev_type;
	data.props = props;
	recursive_cnt = 0;
	notify_next(source, &data);
}

static void _cfm_event_notify(
	struct dvb_frontend *fe,
	enum cfm_notify_event_type_t ev_type,
	struct cfm_properties *props,
	void *notify_data)
{
	int i, id = fe->id;
	u32 data;
	struct dvbdev_fe_event *p = NULL;

	ADAPTER_INFO("event_type = %x fe: %d\n", ev_type, id);
	mutex_lock(&notify_mutex);

	_dump_cfm_props(props);
	p = &_mdrv_adapter->fe_list[id];
	for (i = 0; i < props->num; i++) {
		switch (props->props[i].cmd) {
		case CFM_STREAMING_STATUS:
			data = props->props[i].u.data;
			p->locked = (data) ? true : false;
			break;
		default:
			break;
		}
	}
	switch (ev_type) {
	case STREAMING_STATUS_CHANGED:
		// event notified but media links are not set
		p->status.fe = fe;
		p->status.ev_type = ev_type;
		_copy_cfm_props(props, &p->status.props);
		p->status.notify_data = notify_data;
		break;
	case STREAM_CONFIGURATION_CHANGED:
		// event notified but media links are not set
		p->config.fe = fe;
		p->config.ev_type = ev_type;
		_copy_cfm_props(props, &p->config.props);
		p->config.notify_data = notify_data;
		break;
	default:
		ADAPTER_ERR("not support(ev: %x)\n", ev_type);
		mutex_unlock(&notify_mutex);
		return;
	}
	__cfm_event_notify(fe, ev_type, props, notify_data);
	mutex_unlock(&notify_mutex);
	return;
}

static struct cfm_notify _cfm_notify = {
	.notify = _cfm_event_notify,
	.notify_data = NULL,
};
#endif

struct dvb_adapter *mdrv_get_dvb_adapter(void)
{
	if (!_mdrv_adapter) {
		ADAPTER_ERR("_mdrv_adapter == NULL\n");
		return NULL;
	}

	return &_mdrv_adapter->adapter;
}
EXPORT_SYMBOL(mdrv_get_dvb_adapter);

struct dvb_device *mdrv_adapter_find_device(enum dvb_device_type type, u32 id)
{
	struct dvb_device *dvbdev, *temp;
	struct dvb_adapter *adap = mdrv_get_dvb_adapter();

	if (!adap) {
		ADAPTER_ERR("_mdrv_adapter == NULL\n");
		return NULL;
	}

	list_for_each_entry_safe(dvbdev, temp, &adap->device_list, list_head)
		if ((type == dvbdev->type) && (id == dvbdev->id))
			return dvbdev;

	return NULL;
}
EXPORT_SYMBOL(mdrv_adapter_find_device);

int mdrv_adapter_register_device_notifier(
	struct dvb_device *dev, struct dvbdev_notifier *notifier)
{
#if defined(CONFIG_MEDIA_CONTROLLER_DVB) || defined(CONFIG_MTK_MEDIA_CONTROLLER_DVB)
	dvbdev_hash_type_t type = _get_hash_list_type(dev->entity);
	dvbdev_hash_node_t *node;

	if (type == DVBDEV_HASH_TYPE_NONE)
		return -EINVAL;

	node = devm_kzalloc(
		&_mdrv_adapter->pdev->dev,
		sizeof(dvbdev_hash_node_t),
		GFP_KERNEL);

	if (!node) {
		ADAPTER_ERR("allocate hash node fail\n");
		return -ENOMEM;
	}

	node->dev = dev;
	node->entity = dev->entity;
	memcpy(&node->notifier, notifier, sizeof(node->notifier));

	mutex_lock(&_mdrv_adapter->hash_mutex);
	list_add_tail(&node->list, &_mdrv_adapter->hash_list[type]);
	if (is_ci2_device(dev->entity))
		_mdrv_adapter->cam_inserted = true;
	mutex_unlock(&_mdrv_adapter->hash_mutex);

	dev->entity->ops = &entify_ops;

	return 0;
#else
	return 0;
#endif
}
EXPORT_SYMBOL(mdrv_adapter_register_device_notifier);

int mdrv_adapter_unregister_device_notifier(struct dvb_device *dev)
{
#if defined(CONFIG_MEDIA_CONTROLLER_DVB) || defined(CONFIG_MTK_MEDIA_CONTROLLER_DVB)
	dvbdev_hash_type_t type = _get_hash_list_type(dev->entity);
	dvbdev_hash_node_t *node, *next;
	u32 t;

	if (type == DVBDEV_HASH_TYPE_NONE)
		return -EINVAL;

	t = (u32)type;
	TH_WRITE_LOCK(&_mdrv_adapter->th_lock);

	mutex_lock(&_mdrv_adapter->hash_mutex);

	list_for_each_entry_safe(node, next, &_mdrv_adapter->hash_list[t], list)
		if (node->entity == dev->entity) {
			list_del(&node->list);
			devm_kfree(&_mdrv_adapter->pdev->dev, node);
			if (is_ci2_device(dev->entity))
				_mdrv_adapter->cam_inserted = false;
		}

	mutex_unlock(&_mdrv_adapter->hash_mutex);

	TH_WRITE_UNLOCK(&_mdrv_adapter->th_lock);
	return 0;
#else
	return 0;
#endif
}
EXPORT_SYMBOL(mdrv_adapter_unregister_device_notifier);

int mdrv_adapter_query_device_params(
	struct dvb_device *dev, struct dvbdev_properties *props)
{
#if defined(CONFIG_MEDIA_CONTROLLER_DVB) || defined(CONFIG_MTK_MEDIA_CONTROLLER_DVB)
	int ret, i;
	dvbdev_hash_node_t *node = _get_hash_list_node(dev->entity);
	struct dvbdev_property *prop;
	struct notify_fe_data data;
	struct cfm_property cfm_prop;
	struct cfm_properties cfm_props;

	if (!node) {
		ADAPTER_ERR("get hash node fail\n");
		return -EFAULT;
	}

	ret = call_op(&node->notifier, query_notify, dev, props);
	if (ret < 0) {
		if (is_ci2_device(dev->entity) && (ret == -ENODEV) &&
			(props->props[0].cmd == ADAP_CMD_CI2_CAM_REMOVED_CB)) {
			TH_WRITE_LOCK(&_mdrv_adapter->th_lock);

			_mdrv_adapter->cam_inserted = false;

			TH_WRITE_UNLOCK(&_mdrv_adapter->th_lock);
			ADAPTER_INFO("call_op: (%s,%x) query_notify -ENODEV (remove CB)\n",
				dev->name, dev->id);
			return 0;
		} else {
			ADAPTER_ERR("call_op: (%s,%x) query_notify fail\n",
				dev->name, dev->id);
			return -EFAULT;
		}
	}
	for (i = 0; i < props->num; i++) {
		prop = &props->props[i];
		switch (prop->cmd) {
		case ADAP_CMD_FILEIN_STREAMING_ON:
			if (prop->result) {
				cfm_prop.cmd = CFM_STREAMING_STATUS;
				cfm_prop.u.data = true;
				cfm_props.num = 1;
				cfm_props.props = &cfm_prop;
				data.fe = NULL;
				data.ev_type = STREAMING_STATUS_CHANGED;
				data.props = &cfm_props;
				ret = call_op(&node->notifier, streaming_event_notify,
						dev, data.ev_type, data.props, data.fe);
				if (ret < 0) {
					ADAPTER_ERR("call_op: (%s,%x) streaming_event_notify fail\n",
						dev->name, dev->id);
					return -EFAULT;
				}
				notify_next(dev->entity, &data);
			}
			break;
		default:
			break;
		}
	}
	return ret;
#else
	return 0;
#endif
}
EXPORT_SYMBOL(mdrv_adapter_query_device_params);

static int mdrv_adapter_probe(struct platform_device *pdev)
{
	struct mdrv_dvb_adapter *mdrv_adapter;
	int ret;
	struct device_node *node;
	u32 nr;
	struct sched_param stThrSched = { 0 };

	stThrSched.sched_priority = 40;
	if (_mdrv_adapter != NULL)
		return 0;

	mdrv_adapter = devm_kzalloc(&pdev->dev, sizeof(struct mdrv_dvb_adapter),
		GFP_KERNEL);
	if (!mdrv_adapter) {
		ADAPTER_ERR("allocate dvb adapter fail\n");
		return -ENOMEM;
	}

	of_node_get(pdev->dev.of_node);
	node = of_find_node_by_name(pdev->dev.of_node, "adap_nr");
	if (node != NULL) {
		ret = of_property_read_u32(node, "number", &nr);
		if (ret == 0 && nr != 0xFF)
			adapter_nr[0] = nr;
	}
	of_node_put(node);

	ret = dvb_register_adapter(&mdrv_adapter->adapter, DRV_NAME,
		THIS_MODULE, &pdev->dev, adapter_nr);
	if (ret < 0) {
		ADAPTER_ERR("register dvb adapter fail\n");
		return ret;
	}

#if defined(CONFIG_MEDIA_CONTROLLER_DVB) || defined(CONFIG_MTK_MEDIA_CONTROLLER_DVB)
	mutex_init(&notify_mutex);
	ret = cfm_register_notify(&_cfm_notify);
	if (ret < 0) {
		ADAPTER_ERR("register cfm notify function fail\n");
		return ret;
	}

	ret = _init_mdev(&mdrv_adapter->mdev, &mdrv_adapter->adapter);
	if (ret < 0) {
		ADAPTER_ERR("init media device fail\n");
		return ret;
	}

	_init_hash_list(mdrv_adapter);

	mdrv_adapter->fe_list = devm_kzalloc(&pdev->dev,
		(sizeof(struct dvbdev_fe_event) * FE_MAX), GFP_KERNEL);
#endif

	mdrv_adapter->pdev = pdev;

	platform_set_drvdata(pdev, mdrv_adapter);
	_mdrv_adapter = mdrv_adapter;
	_mdrv_adapter->fragment_h.LTS_id = TS_SYNC;

	_mdrv_adapter->task_cam2null = kthread_run(_ci2_task_cam2null, NULL, "dvbci_cam2null");
	sched_setscheduler_nocheck(_mdrv_adapter->task_cam2null, SCHED_RR, &stThrSched);

	rwlock_init(&_mdrv_adapter->th_lock);
	atomic_set(&_mdrv_adapter->th_lock_wr_cnt, 0);
	atomic_set(&_mdrv_adapter->th_lock_rd_cnt, 0);

	ret = sysfs_create_group(&pdev->dev.kobj, &dvb_adapter_attrs_group);
	if (ret < 0) {
		ADAPTER_ERR("failed to create sysfs group\n");
		return -EFAULT;
	}
	return 0;
}

static int mdrv_adapter_remove(struct platform_device *pdev)
{
	int ret;
	struct mdrv_dvb_adapter *mdrv_adapter = platform_get_drvdata(pdev);

	sysfs_remove_group(&pdev->dev.kobj, &dvb_adapter_attrs_group);

	_mdrv_adapter = NULL;

#if defined(CONFIG_MEDIA_CONTROLLER_DVB) || defined(CONFIG_MTK_MEDIA_CONTROLLER_DVB)
	_release_mdev(&mdrv_adapter->mdev);

	ret = cfm_unregister_notify(&_cfm_notify);
	if (ret < 0) {
		ADAPTER_ERR("un-register cfm notify function fail\n");
		mutex_destroy(&notify_mutex);
		return ret;
	}
	mutex_destroy(&notify_mutex);
#endif

	dvb_unregister_adapter(&mdrv_adapter->adapter);

	return 0;
}

static const struct of_device_id mdrv_adapter_dt_match[] = {
	{ .compatible = DRV_NAME },
	{},
};
MODULE_DEVICE_TABLE(of, mdrv_adapter_dt_match);

static struct platform_driver mdrv_adapter_driver = {
	.probe          = mdrv_adapter_probe,
	.remove         = mdrv_adapter_remove,
	.driver         = {
		.name   = DRV_NAME,
		.owner  = THIS_MODULE,
		.of_match_table = mdrv_adapter_dt_match
	}
};

module_platform_driver(mdrv_adapter_driver);

MODULE_DESCRIPTION("Mediatek DVB Adapter Driver");
MODULE_AUTHOR("Mediatek Author");
MODULE_LICENSE("GPL");
