// SPDX-License-Identifier: GPL-2.0-only
/*
 * Copyright 2021-2024 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
 */

/*
 * Currently this driver does not fully support the serial port of the
 * Extron, only the USB port is fully supported.
 *
 * Issues specific to using the serial port instead of the USB since the
 * serial port doesn't detect if the device is powered off:
 *
 * - Some periodic ping mechanism is needed to detect when the Extron is
 *   powered off and when it is powered on again.
 * - What to do when it is powered off and the driver is modprobed? Keep
 *   trying to contact the Extron indefinitely?
 */

#include <linux/completion.h>
#include <linux/ctype.h>
#include <linux/delay.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/time.h>

#include "extron-da-hd-4k-plus.h"

MODULE_AUTHOR("Hans Verkuil <hansverk@cisco.com>");
MODULE_DESCRIPTION("Extron DA HD 4K PLUS HDMI CEC driver");
MODULE_LICENSE("GPL");

static int debug;
module_param(debug, int, 0644);
MODULE_PARM_DESC(debug, "debug level (0-1)");

static unsigned int vendor_id;
module_param(vendor_id, uint, 0444);
MODULE_PARM_DESC(vendor_id, "CEC Vendor ID");

static char manufacturer_name[4];
module_param_string(manufacturer_name, manufacturer_name,
		    sizeof(manufacturer_name), 0644);
MODULE_PARM_DESC(manufacturer_name,
		 "EDID Vendor String (3 uppercase characters)");

static bool hpd_never_low;
module_param(hpd_never_low, bool, 0644);
MODULE_PARM_DESC(hpd_never_low, "Input HPD will never go low (1), or go low if all output HPDs are low (0, default)");

#define EXTRON_TIMEOUT_SECS 6

static const u8 hdmi_edid[256] = {
	0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00,
	0x31, 0xd8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x01, 0x20, 0x01, 0x03, 0x80, 0x60, 0x36, 0x78,
	0x0f, 0xee, 0x91, 0xa3, 0x54, 0x4c, 0x99, 0x26,
	0x0f, 0x50, 0x54, 0x20, 0x00, 0x00, 0x01, 0x01,
	0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
	0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x3a,
	0x80, 0x18, 0x71, 0x38, 0x2d, 0x40, 0x58, 0x2c,
	0x45, 0x00, 0xc0, 0x1c, 0x32, 0x00, 0x00, 0x1e,
	0x00, 0x00, 0x00, 0xfd, 0x00, 0x18, 0x55, 0x18,
	0x87, 0x11, 0x00, 0x0a, 0x20, 0x20, 0x20, 0x20,
	0x20, 0x20, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x68,
	0x64, 0x6d, 0x69, 0x2d, 0x31, 0x30, 0x38, 0x30,
	0x70, 0x36, 0x30, 0x0a, 0x00, 0x00, 0x00, 0xfe,
	0x00, 0x73, 0x65, 0x72, 0x69, 0x6f, 0x0a, 0x20,
	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x01, 0x95,

	0x02, 0x03, 0x1b, 0xf1, 0x42, 0x10, 0x01, 0x23,
	0x09, 0x07, 0x07, 0x83, 0x01, 0x00, 0x00, 0x68,
	0x03, 0x0c, 0x00, 0x10, 0x00, 0x00, 0x21, 0x01,
	0xe2, 0x00, 0xca, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x89,
};

static const u8 hdmi_edid_4k_300[256] = {
	0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00,
	0x31, 0xd8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x01, 0x20, 0x01, 0x03, 0x80, 0x60, 0x36, 0x78,
	0x0f, 0xee, 0x91, 0xa3, 0x54, 0x4c, 0x99, 0x26,
	0x0f, 0x50, 0x54, 0x20, 0x00, 0x00, 0x01, 0x01,
	0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
	0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x3a,
	0x80, 0x18, 0x71, 0x38, 0x2d, 0x40, 0x58, 0x2c,
	0x45, 0x00, 0xc0, 0x1c, 0x32, 0x00, 0x00, 0x1e,
	0x00, 0x00, 0x00, 0xfd, 0x00, 0x18, 0x55, 0x18,
	0x87, 0x3c, 0x00, 0x0a, 0x20, 0x20, 0x20, 0x20,
	0x20, 0x20, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x68,
	0x64, 0x6d, 0x69, 0x2d, 0x34, 0x6b, 0x2d, 0x36,
	0x30, 0x30, 0x0a, 0x20, 0x00, 0x00, 0x00, 0xfe,
	0x00, 0x73, 0x65, 0x72, 0x69, 0x6f, 0x0a, 0x20,
	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x01, 0x87,

	0x02, 0x03, 0x1f, 0xf1, 0x43, 0x10, 0x5f, 0x01,
	0x23, 0x09, 0x07, 0x07, 0x83, 0x01, 0x00, 0x00,
	0x6b, 0x03, 0x0c, 0x00, 0x10, 0x00, 0x00, 0x3c,
	0x21, 0x00, 0x20, 0x01, 0xe2, 0x00, 0xca, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc6,
};

static const u8 hdmi_edid_4k_600[256] = {
	0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00,
	0x31, 0xd8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x01, 0x20, 0x01, 0x03, 0x80, 0x60, 0x36, 0x78,
	0x0f, 0xee, 0x91, 0xa3, 0x54, 0x4c, 0x99, 0x26,
	0x0f, 0x50, 0x54, 0x20, 0x00, 0x00, 0x01, 0x01,
	0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
	0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x08, 0xe8,
	0x00, 0x30, 0xf2, 0x70, 0x5a, 0x80, 0xb0, 0x58,
	0x8a, 0x00, 0xc0, 0x1c, 0x32, 0x00, 0x00, 0x1e,
	0x00, 0x00, 0x00, 0xfd, 0x00, 0x18, 0x55, 0x18,
	0x87, 0x3c, 0x00, 0x0a, 0x20, 0x20, 0x20, 0x20,
	0x20, 0x20, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x68,
	0x64, 0x6d, 0x69, 0x2d, 0x34, 0x6b, 0x2d, 0x36,
	0x30, 0x30, 0x0a, 0x20, 0x00, 0x00, 0x00, 0xfe,
	0x00, 0x73, 0x65, 0x72, 0x69, 0x6f, 0x0a, 0x20,
	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x01, 0x4c,

	0x02, 0x03, 0x28, 0xf1, 0x44, 0x61, 0x5f, 0x10,
	0x01, 0x23, 0x09, 0x07, 0x07, 0x83, 0x01, 0x00,
	0x00, 0x6b, 0x03, 0x0c, 0x00, 0x10, 0x00, 0x00,
	0x3c, 0x21, 0x00, 0x20, 0x01, 0x67, 0xd8, 0x5d,
	0xc4, 0x01, 0x78, 0x00, 0x00, 0xe2, 0x00, 0xca,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x82,
};

static int extron_send_byte(struct serio *serio, char byte)
{
	int err, i;

	for (i = 0; i < 100; i++) {
		err = serio_write(serio, byte);
		if (!err)
			break;
		usleep_range(80, 120);
	}
	if (err)
		dev_warn(&serio->dev, "unable to write byte after 100 attempts\n");
	return err ? -EIO : 0;
}

static int extron_send_len(struct serio *serio, const char *command,
			   const unsigned char *bin, unsigned int len)
{
	int err = 0;

	for (; !err && *command; command++)
		err = extron_send_byte(serio, *command);
	if (!err)
		err = extron_send_byte(serio, '\r');
	if (bin)
		for (; !err && len; len--)
			err = extron_send_byte(serio, *bin++);
	return err;
}

static int extron_send_and_wait_len(struct extron *extron, struct extron_port *port,
				    const char *cmd, const unsigned char *bin,
				    unsigned int len, const char *response)
{
	int timeout = EXTRON_TIMEOUT_SECS * HZ;
	int err;

	if (debug) {
		if (response)
			dev_info(extron->dev, "transmit %s (response: %s)\n",
				 cmd, response);
		else
			dev_info(extron->dev, "transmit %s\n", cmd);
	}

	mutex_lock(&extron->serio_lock);
	if (port) {
		init_completion(&port->cmd_done);
		port->cmd_error = 0;
		port->response = response;
	} else {
		init_completion(&extron->cmd_done);
		extron->cmd_error = 0;
		extron->response = response;
	}
	err = extron_send_len(extron->serio, cmd, bin, len);

	if (!err && response &&
	    !wait_for_completion_timeout(port ? &port->cmd_done : &extron->cmd_done, timeout)) {
		dev_info(extron->dev, "transmit %s failed with %s (expected: %s)\n",
			 cmd, extron->reply, response);
		err = -ETIMEDOUT;
	}

	if (!err && response && (port ? port->cmd_error : extron->cmd_error)) {
		dev_info(extron->dev, "transmit %s failed with E%02u (expected: %s)\n",
			 cmd, port ? port->cmd_error : extron->cmd_error, response);
		if (port)
			port->cmd_error = 0;
		else
			extron->cmd_error = 0;
		err = -EPROTO;
	}
	if (port)
		port->response = NULL;
	else
		extron->response = NULL;
	mutex_unlock(&extron->serio_lock);
	return err;
}

static int extron_send_and_wait(struct extron *extron, struct extron_port *port,
				const char *cmd, const char *response)
{
	return extron_send_and_wait_len(extron, port, cmd, NULL, 0, response);
}

static void extron_parse_edid(struct extron_port *port)
{
	const u8 *edid = port->edid;
	unsigned int i, end;
	u8 d;

	port->has_4kp30 = false;
	port->has_4kp60 = false;
	port->has_qy = false;
	port->has_qs = false;
	/* Store Established Timings 1 and 2 */
	port->est_i = edid[0x23];
	port->est_ii = edid[0x24];

	// Check DTDs in base block
	for (i = 0; i < 4; i++) {
		const u8 *dtd = edid + 0x36 + i * 18;
		unsigned int w, h;
		unsigned int mhz;
		u64 pclk;

		if (!dtd[0] && !dtd[1])
			continue;
		w = dtd[2] + ((dtd[4] & 0xf0) << 4);
		h = dtd[5] + ((dtd[7] & 0xf0) << 4);
		if (w != 3840 || h != 2160)
			continue;

		w += dtd[3] + ((dtd[4] & 0x0f) << 8);
		h += dtd[6] + ((dtd[7] & 0x0f) << 8);
		pclk = dtd[0] + (dtd[1] << 8);
		pclk *= 100000;
		mhz = div_u64(pclk, w * h);
		if (mhz >= 297)
			port->has_4kp30 = true;
		if (mhz >= 594)
			port->has_4kp60 = true;
	}

	if (port->edid_blocks == 1)
		return;

	edid += 128;

	/* Return if not a CTA-861 extension block */
	if (edid[0] != 0x02 || edid[1] != 0x03)
		return;

	/* search Video Data Block (tag 2) */
	d = edid[2] & 0x7f;
	/* Check if there are Data Blocks */
	if (d <= 4)
		return;

	i = 4;
	end = d;

	do {
		u8 tag = edid[i] >> 5;
		u8 len = edid[i] & 0x1f;

		/* Avoid buffer overrun in case the EDID is malformed */
		if (i + len + 1 > 0x7f)
			return;

		switch (tag) {
		case 2: /* Video Data Block */
			/* Search for VIC 97 */
			if (memchr(edid + i + 1, 97, len))
				port->has_4kp60 = true;
			/* Search for VIC 95 */
			if (memchr(edid + i + 1, 95, len))
				port->has_4kp30 = true;
			break;

		case 7: /* Use Extended Tag */
			switch (edid[i + 1]) {
			case 0: /* Video Capability Data Block */
				if (edid[i + 2] & 0x80)
					port->has_qy = true;
				if (edid[i + 2] & 0x40)
					port->has_qs = true;
				break;
			}
			break;
		}
		i += len + 1;
	} while (i < end);
}

static int get_edid_tag_location(const u8 *edid, unsigned int size,
				 u8 want_tag, u8 ext_tag)
{
	unsigned int offset = 128;
	int i, end;
	u8 d;

	edid += offset;

	/* Return if not a CTA-861 extension block */
	if (size < 256 || edid[0] != 0x02 || edid[1] != 0x03)
		return -ENOENT;

	/* search tag */
	d = edid[0x02] & 0x7f;
	if (d <= 4)
		return -ENOENT;

	i = 0x04;
	end = 0x00 + d;

	do {
		unsigned char tag = edid[i] >> 5;
		unsigned char len = edid[i] & 0x1f;

		if (tag != want_tag || i + len > end) {
			i += len + 1;
			continue;
		}

		if (tag < 7 || (len >= 1 && edid[i + 1] == ext_tag))
			return offset + i;
		i += len + 1;
	} while (i < end);
	return -ENOENT;
}

static void extron_edid_crc(u8 *edid)
{
	u8 sum = 0;
	int offset;

	/* Update CRC */
	for (offset = 0; offset < 127; offset++)
		sum += edid[offset];
	edid[127] = 256 - sum;
}

/*
 * Fill in EDID string. As per VESA EDID-1.3, strings are at most 13 chars
 * long. If shorter then add a 0x0a character after the string and pad the
 * remainder with spaces.
 */
static void extron_set_edid_string(u8 *start, const char *s)
{
	const unsigned int max_len = 13;
	int len = strlen(s);

	memset(start, ' ', max_len);
	if (len > max_len)
		len = max_len;
	memcpy(start, s, len);
	if (len < max_len)
		start[len] = 0x0a;
}

static void extron_update_edid(struct extron_port *port, unsigned int blocks)
{
	int offset;
	u8 c1, c2;

	c1 = ((manufacturer_name[0] - '@') << 2) |
		(((manufacturer_name[1] - '@') >> 3) & 0x03);
	c2 = (((manufacturer_name[1] - '@') & 0x07) << 5) |
		((manufacturer_name[2] - '@') & 0x1f);

	port->edid_tmp[8] = c1;
	port->edid_tmp[9] = c2;

	/* Set Established Timings, but always enable VGA */
	port->edid_tmp[0x23] = port->est_i | 0x20;
	port->edid_tmp[0x24] = port->est_ii;

	/* Set the Monitor Name to the unit name */
	extron_set_edid_string(port->edid_tmp + 0x5f, port->extron->unit_name);
	/* Set the ASCII String to the CEC adapter name */
	extron_set_edid_string(port->edid_tmp + 0x71, port->adap->name);

	extron_edid_crc(port->edid_tmp);

	/* Find Video Capability Data Block */
	offset = get_edid_tag_location(port->edid_tmp, blocks * 128, 7, 0);
	if (offset > 0) {
		port->edid_tmp[offset + 2] &= ~0xc0;
		if (port->has_qy)
			port->edid_tmp[offset + 2] |= 0x80;
		if (port->has_qs)
			port->edid_tmp[offset + 2] |= 0x40;
	}

	extron_edid_crc(port->edid_tmp + 128);
}

static int extron_write_edid(struct extron_port *port,
			     const u8 *edid, unsigned int blocks)
{
	struct extron *extron = port->extron;
	u16 phys_addr = CEC_PHYS_ADDR_INVALID;
	int ret;

	if (cec_get_edid_spa_location(edid, blocks * 128))
		phys_addr = 0;

	if (mutex_lock_interruptible(&extron->edid_lock))
		return -EINTR;

	memcpy(port->edid_tmp, edid, blocks * 128);

	if (manufacturer_name[0])
		extron_update_edid(port, blocks);

	ret = extron_send_and_wait_len(port->extron, port, "W+UF256,in.bin",
				       port->edid_tmp, sizeof(port->edid_tmp),
				       "Upl");
	if (ret)
		goto unlock;
	ret = extron_send_and_wait(port->extron, port, "WI1,in.binEDID",
				   "EdidI01");
	if (ret)
		goto unlock;

	port->edid_blocks = blocks;
	memcpy(port->edid, port->edid_tmp, blocks * 128);
	port->read_edid = true;
	mutex_unlock(&extron->edid_lock);

	cec_s_phys_addr(port->adap, phys_addr, false);
	return 0;

unlock:
	mutex_unlock(&extron->edid_lock);
	return ret;
}

static void update_edid_work(struct work_struct *w)
{
	struct extron *extron = container_of(w, struct extron,
					     work_update_edid.work);
	struct extron_port *in = extron->ports[extron->num_out_ports];
	struct extron_port *p;
	bool has_edid = false;
	bool has_4kp30 = true;
	bool has_4kp60 = true;
	bool has_qy = true;
	bool has_qs = true;
	u8 est_i = 0xff;
	u8 est_ii = 0xff;
	unsigned int out;

	for (out = 0; has_4kp60 && out < extron->num_out_ports; out++) {
		p = extron->ports[out];
		if (p->read_edid) {
			has_4kp60 = p->has_4kp60;
			est_i &= p->est_i;
			est_ii &= p->est_ii;
			has_edid = true;
		}
	}
	for (out = 0; has_4kp30 && out < extron->num_out_ports; out++)
		if (extron->ports[out]->read_edid)
			has_4kp30 = extron->ports[out]->has_4kp30;

	for (out = 0; has_qy && out < extron->num_out_ports; out++)
		if (extron->ports[out]->read_edid)
			has_qy = extron->ports[out]->has_qy;

	for (out = 0; has_qs && out < extron->num_out_ports; out++)
		if (extron->ports[out]->read_edid)
			has_qs = extron->ports[out]->has_qs;

	/* exit if no output port had an EDID */
	if (!has_edid)
		return;

	/* exit if the input EDID properties remained unchanged */
	if (has_4kp60 == in->has_4kp60 && has_4kp30 == in->has_4kp30 &&
	    has_qy == in->has_qy && has_qs == in->has_qs &&
	    est_i == in->est_i && est_ii == in->est_ii)
		return;

	in->has_4kp60 = has_4kp60;
	in->has_4kp30 = has_4kp30;
	in->has_qy = has_qy;
	in->has_qs = has_qs;
	in->est_i = est_i;
	in->est_ii = est_ii;
	extron_write_edid(extron->ports[extron->num_out_ports],
			  has_4kp60 ? hdmi_edid_4k_600 :
			  (has_4kp30 ? hdmi_edid_4k_300 : hdmi_edid), 2);
}

static void extron_read_edid(struct extron_port *port)
{
	struct extron *extron = port->extron;
	char cmd[10], reply[10];
	unsigned int idx;

	idx = port->port.port + (port->is_input ? 0 : extron->num_in_ports);
	snprintf(cmd, sizeof(cmd), "WR%uEDID", idx);
	snprintf(reply, sizeof(reply), "EdidR%u", idx);
	if (mutex_lock_interruptible(&extron->edid_lock))
		return;
	if (port->read_edid)
		goto unlock;
	extron->edid_bytes_read = 0;
	extron->edid_port = port;
	port->edid_blocks = 0;
	if (!port->has_edid)
		goto no_edid;

	extron->edid_reading = true;

	if (!extron_send_and_wait(extron, port, cmd, reply))
		wait_for_completion_killable_timeout(&extron->edid_completion,
						     msecs_to_jiffies(1000));
	if (port->edid_blocks) {
		extron_parse_edid(port);
		port->read_edid = true;
		if (!port->is_input)
			v4l2_ctrl_s_ctrl(port->ctrl_tx_edid_present, 1);
	}
no_edid:
	extron->edid_reading = false;
unlock:
	mutex_unlock(&extron->edid_lock);
	cancel_delayed_work_sync(&extron->work_update_edid);
	if (manufacturer_name[0])
		schedule_delayed_work(&extron->work_update_edid,
				      msecs_to_jiffies(1000));
}

static void extron_irq_work_handler(struct work_struct *work)
{
	struct extron_port *port =
		container_of(work, struct extron_port, irq_work);
	struct extron *extron = port->extron;
	unsigned long flags;
	bool update_pa;
	u16 pa;
	bool update_has_signal;
	bool has_signal;
	bool update_has_edid;
	bool has_edid;
	u32 status;

	spin_lock_irqsave(&port->msg_lock, flags);
	while (port->rx_msg_num) {
		spin_unlock_irqrestore(&port->msg_lock, flags);
		cec_received_msg(port->adap,
				 &port->rx_msg[port->rx_msg_cur_idx]);
		spin_lock_irqsave(&port->msg_lock, flags);
		if (port->rx_msg_num)
			port->rx_msg_num--;
		port->rx_msg_cur_idx =
			(port->rx_msg_cur_idx + 1) % NUM_MSGS;
	}
	update_pa = port->update_phys_addr;
	pa = port->phys_addr;
	port->update_phys_addr = false;
	update_has_signal = port->update_has_signal;
	has_signal = port->has_signal;
	port->update_has_signal = false;
	update_has_edid = port->update_has_edid;
	has_edid = port->has_edid;
	port->update_has_edid = false;
	status = port->tx_done_status;
	port->tx_done_status = 0;
	spin_unlock_irqrestore(&port->msg_lock, flags);

	if (status)
		cec_transmit_done(port->adap, status, 0, 0, 0, 0);

	if (update_has_signal && port->is_input)
		v4l2_ctrl_s_ctrl(port->ctrl_rx_power_present, has_signal);

	if (update_has_edid && !port->is_input) {
		v4l2_ctrl_s_ctrl(port->ctrl_tx_hotplug,
				 port->has_edid);
		if (port->has_edid) {
			port->port.found_sink = true;
			port->port.lost_sink_ts = ktime_set(0, 0);
		} else {
			port->port.lost_sink_ts = ktime_get();
		}
		if (!has_edid) {
			port->edid_blocks = 0;
			port->read_edid = false;
			if (extron->edid_reading && !has_edid &&
			    extron->edid_port == port)
				extron->edid_reading = false;
			v4l2_ctrl_s_ctrl(port->ctrl_tx_edid_present, 0);
		} else if (!extron->edid_reading || extron->edid_port != port) {
			extron_read_edid(port);
		}
	}
	if (update_pa)
		cec_s_phys_addr(port->adap, pa, false);
}

static void extron_process_received(struct extron_port *port, const char *data)
{
	struct cec_msg msg = {};
	unsigned int len = strlen(data);
	unsigned long irq_flags;
	unsigned int idx;

	if (!port || port->disconnected)
		return;

	if (len < 5 || (len - 2) % 3 || data[len - 2] != '*')
		goto malformed;

	while (*data != '*') {
		int v = hex2bin(&msg.msg[msg.len], data + 1, 1);

		if (*data != '%' || v)
			goto malformed;
		msg.len++;
		data += 3;
	}

	spin_lock_irqsave(&port->msg_lock, irq_flags);
	idx = (port->rx_msg_cur_idx + port->rx_msg_num) %
		NUM_MSGS;
	if (port->rx_msg_num == NUM_MSGS) {
		dev_warn(port->dev,
			 "message queue is full, dropping %*ph\n",
			 msg.len, msg.msg);
		spin_unlock_irqrestore(&port->msg_lock,
				       irq_flags);
		return;
	}
	port->rx_msg_num++;
	port->rx_msg[idx] = msg;
	spin_unlock_irqrestore(&port->msg_lock, irq_flags);
	if (!port->disconnected)
		schedule_work(&port->irq_work);
	return;

malformed:
	dev_info(port->extron->dev, "malformed msg received: '%s'\n", data);
}

static void extron_port_signal_change(struct extron_port *port, bool has_sig)
{
	unsigned long irq_flags;
	bool update = false;

	if (!port)
		return;

	spin_lock_irqsave(&port->msg_lock, irq_flags);
	if (!port->update_has_signal && port->has_signal != has_sig) {
		port->update_has_signal = true;
		update = true;
	}
	port->has_signal = has_sig;
	spin_unlock_irqrestore(&port->msg_lock, irq_flags);
	if (update && !port->disconnected)
		schedule_work(&port->irq_work);
}

static void extron_process_signal_change(struct extron *extron, const char *data)
{
	unsigned int i;

	extron_port_signal_change(extron->ports[extron->num_out_ports],
				  data[0] == '1');
	for (i = 0; i < extron->num_out_ports; i++)
		extron_port_signal_change(extron->ports[i],
					  data[2 + 2 * i] != '0');
}

static void extron_port_edid_change(struct extron_port *port, bool has_edid)
{
	unsigned long irq_flags;
	bool update = false;

	if (!port)
		return;

	spin_lock_irqsave(&port->msg_lock, irq_flags);
	if (!port->update_has_edid && port->has_edid != has_edid) {
		port->update_has_edid = true;
		update = true;
	}
	port->has_edid = has_edid;
	spin_unlock_irqrestore(&port->msg_lock, irq_flags);
	if (update && !port->disconnected)
		schedule_work(&port->irq_work);
}

static void extron_process_edid_change(struct extron *extron, const char *data)
{
	unsigned int i;

	/*
	 * Do nothing if the Extron isn't ready yet. Trying to do this
	 * while the Extron firmware is still settling will fail.
	 */
	if (!extron->is_ready)
		return;

	for (i = 0; i < extron->num_out_ports; i++)
		extron_port_edid_change(extron->ports[i],
					data[2 + 2 * i] != '0');
}

static void extron_phys_addr_change(struct extron_port *port, u16 pa)
{
	unsigned long irq_flags;
	bool update = false;

	if (!port)
		return;

	spin_lock_irqsave(&port->msg_lock, irq_flags);
	if (!port->update_phys_addr && port->phys_addr != pa) {
		update = true;
		port->update_phys_addr = true;
	}
	port->phys_addr = pa;
	spin_unlock_irqrestore(&port->msg_lock, irq_flags);
	if (update && !port->disconnected)
		schedule_work(&port->irq_work);
}

static void extron_process_tx_done(struct extron_port *port, char status)
{
	unsigned long irq_flags;
	unsigned int tx_status;

	if (!port)
		return;

	switch (status) {
	case '0':
		tx_status = CEC_TX_STATUS_NACK | CEC_TX_STATUS_MAX_RETRIES;
		break;
	case '1':
		tx_status = CEC_TX_STATUS_OK;
		break;
	default:
		tx_status = CEC_TX_STATUS_ERROR;
		break;
	}
	spin_lock_irqsave(&port->msg_lock, irq_flags);
	port->tx_done_status = tx_status;
	spin_unlock_irqrestore(&port->msg_lock, irq_flags);
	if (!port->disconnected)
		schedule_work(&port->irq_work);
}

static void extron_add_edid(struct extron_port *port, const char *hex)
{
	struct extron *extron = port ? port->extron : NULL;

	if (!port || port != extron->edid_port)
		return;
	while (extron->edid_bytes_read < sizeof(port->edid) && *hex) {
		int err = hex2bin(&port->edid[extron->edid_bytes_read], hex, 1);

		if (err) {
			extron->edid_reading = false;
			complete(&extron->edid_completion);
			break;
		}
		extron->edid_bytes_read++;
		hex += 2;
	}
	if (extron->edid_bytes_read == 128 &&
	    port->edid[126] == 0) {
		/* There are no extension blocks, we're done */
		port->edid_blocks = 1;
		extron->edid_reading = false;
		complete(&extron->edid_completion);
	}
	if (extron->edid_bytes_read < sizeof(port->edid))
		return;
	if (!*hex)
		port->edid_blocks = 2;
	extron->edid_reading = false;
	complete(&extron->edid_completion);
}

static irqreturn_t extron_interrupt(struct serio *serio, unsigned char data,
				    unsigned int flags)
{
	struct extron *extron = serio_get_drvdata(serio);
	struct extron_port *port = NULL;
	bool found_response;
	unsigned int p;

	if (data == '\r' || data == '\n') {
		if (extron->idx == 0)
			return IRQ_HANDLED;
		memcpy(extron->data, extron->buf, extron->idx);
		extron->len = extron->idx;
		extron->data[extron->len] = 0;
		if (debug)
			dev_info(extron->dev, "received %s\n", extron->data);
		extron->idx = 0;
		if (!memcmp(extron->data, "Sig", 3) &&
		    extron->data[4] == '*') {
			extron_process_signal_change(extron, extron->data + 3);
		} else if (!memcmp(extron->data, "Hdcp", 4) &&
			   extron->data[5] == '*') {
			extron_process_edid_change(extron, extron->data + 4);
		} else if (!memcmp(extron->data, "DcecI", 5) &&
			   extron->data[5] >= '1' &&
			   extron->data[5] < '1' + extron->num_in_ports) {
			unsigned int p = extron->data[5] - '1';

			p += extron->num_out_ports;
			extron_process_tx_done(extron->ports[p],
					       extron->data[extron->len - 1]);
		} else if (!memcmp(extron->data, "Ceci", 4) &&
			   extron->data[4] >= '1' &&
			   extron->data[4] < '1' + extron->num_in_ports &&
			   extron->data[5] == '*') {
			unsigned int p = extron->data[4] - '1';

			p += extron->num_out_ports;
			extron_process_received(extron->ports[p],
						extron->data + 6);
		} else if (!memcmp(extron->data, "DcecO", 5) &&
			   extron->data[5] >= '1' &&
			   extron->data[5] < '1' + extron->num_out_ports) {
			unsigned int p = extron->data[5] - '1';

			extron_process_tx_done(extron->ports[p],
					       extron->data[extron->len - 1]);
		} else if (!memcmp(extron->data, "Ceco", 4) &&
			   extron->data[4] >= '1' &&
			   extron->data[4] < '1' + extron->num_out_ports &&
			   extron->data[5] == '*') {
			unsigned int p = extron->data[4] - '1';

			extron_process_received(extron->ports[p],
						extron->data + 6);
		} else if (!memcmp(extron->data, "Pceco", 5) &&
			   extron->data[5] >= '1' &&
			   extron->data[5] < '1' + extron->num_out_ports) {
			unsigned int p = extron->data[5] - '1';
			unsigned int tmp_pa[2] = { 0xff, 0xff };

			if (sscanf(extron->data + 7, "%%%02x%%%02x",
				   &tmp_pa[0], &tmp_pa[1]) == 2)
				extron_phys_addr_change(extron->ports[p],
							tmp_pa[0] << 8 | tmp_pa[1]);
		} else if (!memcmp(extron->data, "Pceci", 5) &&
			   extron->data[5] >= '1' &&
			   extron->data[5] < '1' + extron->num_in_ports) {
			unsigned int p = extron->data[5] - '1';
			unsigned int tmp_pa[2] = { 0xff, 0xff };

			p += extron->num_out_ports;
			if (sscanf(extron->data + 7, "%%%02x%%%02x",
				   &tmp_pa[0], &tmp_pa[1]) == 2)
				extron_phys_addr_change(extron->ports[p],
							tmp_pa[0] << 8 | tmp_pa[1]);
		} else if (!memcmp(extron->data, "EdidR", 5) &&
			   extron->data[5] >= '1' &&
			   extron->data[5] < '1' + extron->num_ports &&
			   extron->data[6] == '*') {
			unsigned int p = extron->data[5] - '1';

			if (p)
				p--;
			else
				p = extron->num_out_ports;
			extron_add_edid(extron->ports[p], extron->data + 7);
		} else if (extron->edid_reading && extron->len == 32 &&
			   extron->edid_port) {
			extron_add_edid(extron->edid_port, extron->data);
		}

		found_response = false;
		if (extron->response &&
		    !strncmp(extron->response, extron->data,
			     strlen(extron->response)))
			found_response = true;

		for (p = 0; !found_response && p < extron->num_ports; p++) {
			port = extron->ports[p];
			if (port && port->response &&
			    !strncmp(port->response, extron->data,
				     strlen(port->response)))
				found_response = true;
		}

		if (!found_response && extron->response &&
		    extron->data[0] == 'E' &&
		    isdigit(extron->data[1]) &&
		    isdigit(extron->data[2]) &&
		    !extron->data[3]) {
			extron->cmd_error = (extron->data[1] - '0') * 10 +
				extron->data[2] - '0';
			extron->response = NULL;
			complete(&extron->cmd_done);
		}

		if (!found_response)
			return IRQ_HANDLED;

		memcpy(extron->reply, extron->data, extron->len);
		extron->reply[extron->len] = 0;
		if (!port) {
			extron->response = NULL;
			complete(&extron->cmd_done);
		} else {
			port->response = NULL;
			complete(&port->cmd_done);
		}
		return IRQ_HANDLED;
	}

	if (extron->idx >= DATA_SIZE - 1) {
		dev_info(extron->dev,
			 "throwing away %d bytes of garbage\n", extron->idx);
		extron->idx = 0;
	}
	extron->buf[extron->idx++] = (char)data;
	return IRQ_HANDLED;
}

static int extron_cec_adap_enable(struct cec_adapter *adap, bool enable)
{
	struct extron_port *port = cec_get_drvdata(adap);

	return (port->disconnected && enable) ? -ENODEV : 0;
}

static int extron_cec_adap_log_addr(struct cec_adapter *adap, u8 log_addr)
{
	struct extron_port *port = cec_get_drvdata(adap);
	char cmd[26];
	char resp[25];
	u8 la = log_addr == CEC_LOG_ADDR_INVALID ? 15 : log_addr;
	int err;

	if (port->disconnected)
		return -ENODEV;
	snprintf(cmd, sizeof(cmd), "W%c%u*%uLCEC",
		 port->direction, port->port.port, la);
	snprintf(resp, sizeof(resp), "Lcec%c%u*%u",
		 port->direction, port->port.port, la);
	err = extron_send_and_wait(port->extron, port, cmd, resp);
	return log_addr != CEC_LOG_ADDR_INVALID && err ? err : 0;
}

static int extron_cec_adap_transmit(struct cec_adapter *adap, u8 attempts,
				    u32 signal_free_time, struct cec_msg *msg)
{
	struct extron_port *port = cec_get_drvdata(adap);
	char buf[CEC_MAX_MSG_SIZE * 3 + 1];
	char cmd[CEC_MAX_MSG_SIZE * 3 + 13];
	unsigned int i;

	if (port->disconnected)
		return -ENODEV;
	buf[0] = 0;
	for (i = 0; i < msg->len - 1; i++)
		sprintf(buf + i * 3, "%%%02X", msg->msg[i + 1]);
	snprintf(cmd, sizeof(cmd), "W%c%u*%u*%u*%sDCEC",
		 port->direction, port->port.port,
		 cec_msg_initiator(msg), cec_msg_destination(msg), buf);
	return extron_send_and_wait(port->extron, port, cmd, NULL);
}

static void extron_cec_adap_unconfigured(struct cec_adapter *adap)
{
	struct extron_port *port = cec_get_drvdata(adap);

	if (port->disconnected)
		return;
	if (debug)
		dev_info(port->extron->dev, "unconfigured port %d (%s)\n",
			 port->port.port,
			 port->extron->splitter.is_standby ? "Off" : "On");
	if (!port->is_input)
		cec_splitter_unconfigured_output(&port->port);
}

static void extron_cec_configured(struct cec_adapter *adap)
{
	struct extron_port *port = cec_get_drvdata(adap);

	if (port->disconnected)
		return;
	if (debug)
		dev_info(port->extron->dev, "configured port %d (%s)\n",
			 port->port.port,
			 port->extron->splitter.is_standby ? "Off" : "On");
	if (!port->is_input)
		cec_splitter_configured_output(&port->port);
}

static void extron_cec_adap_nb_transmit_canceled(struct cec_adapter *adap,
						 const struct cec_msg *msg)
{
	struct extron_port *port = cec_get_drvdata(adap);
	struct cec_adapter *input_adap;

	if (!vendor_id)
		return;
	if (port->disconnected || port->is_input)
		return;
	input_adap = port->extron->ports[port->extron->num_out_ports]->adap;
	cec_splitter_nb_transmit_canceled_output(&port->port, msg, input_adap);
}

static int extron_received(struct cec_adapter *adap, struct cec_msg *msg)
{
	struct extron_port *port = cec_get_drvdata(adap);

	if (!vendor_id)
		return -ENOMSG;
	if (port->disconnected)
		return -ENOMSG;
	if (port->is_input)
		return cec_splitter_received_input(&port->port, msg);
	return cec_splitter_received_output(&port->port, msg,
					    port->extron->ports[port->extron->num_out_ports]->adap);
}

#define log_printf(adap, file, fmt, arg...) \
	do { \
		if (file) \
			seq_printf((file), fmt, ## arg); \
		else \
			pr_info("cec-%s: " fmt, (adap)->name, ## arg);	\
	} while (0)

static const char * const pwr_state[] = {
	"on",
	"standby",
	"to on",
	"to standby",
};

static void extron_adap_status_port(struct extron_port *port, struct seq_file *file)
{
	struct cec_adapter *adap = port->adap;

	if (port->disconnected) {
		log_printf(adap, file,
			   "\tport %u: disconnected\n", port->port.port);
		return;
	}
	if (port->is_input)
		log_printf(adap, file,
			   "\tport %u: %s signal, %s edid, %s 4kp30, %s 4kp60, %sQS/%sQY, is %s\n",
			   port->port.port,
			   port->has_signal ? "has" : "no",
			   port->has_edid ? "has" : "no",
			   port->has_4kp30 ? "has" : "no",
			   port->has_4kp60 ? "has" : "no",
			   port->has_qs ? "" : "no ",
			   port->has_qy ? "" : "no ",
			   !port->port.adap->is_configured ? "not configured" :
				pwr_state[port->extron->splitter.is_standby]);
	else
		log_printf(adap, file,
			   "\tport %u: %s sink, %s signal, %s edid, %s 4kp30, %s 4kp60, %sQS/%sQY, is %sactive source, is %s\n",
			   port->port.port,
			   port->port.found_sink ? "found" : "no",
			   port->has_signal ? "has" : "no",
			   port->has_edid ? "has" : "no",
			   port->has_4kp30 ? "has" : "no",
			   port->has_4kp60 ? "has" : "no",
			   port->has_qs ? "" : "no ",
			   port->has_qy ? "" : "no ",
			   port->port.is_active_source ? "" : "not ",
			   !port->port.adap->is_configured ? "not configured" :
			   pwr_state[port->port.power_status & 3]);
	if (port->port.out_give_device_power_status_seq)
		log_printf(adap, file,
			   "\tport %u: querying power status (%u, %lldms)\n",
			   port->port.port,
			   port->port.out_give_device_power_status_seq & ~(1 << 31),
			   ktime_ms_delta(ktime_get(),
					  port->port.out_give_device_power_status_ts));
	if (port->port.out_request_current_latency_seq)
		log_printf(adap, file,
			   "\tport %u: querying latency (%u, %lldms)\n",
			   port->port.port,
			   port->port.out_request_current_latency_seq & ~(1 << 31),
			   ktime_ms_delta(ktime_get(),
					  port->port.out_request_current_latency_ts));
}

static void extron_adap_status(struct cec_adapter *adap, struct seq_file *file)
{
	struct extron_port *port = cec_get_drvdata(adap);
	struct extron *extron = port->extron;
	unsigned int i;

	log_printf(adap, file, "name: %s type: %s\n",
		   extron->unit_name, extron->unit_type);
	log_printf(adap, file, "model: 60-160%c-01 (1 input, %u outputs)\n",
		   '6' + extron->num_out_ports / 2, extron->num_out_ports);
	log_printf(adap, file, "firmware version: %s CEC engine version: %s\n",
		   extron->unit_fw_version, extron->unit_cec_engine_version);
	if (extron->hpd_never_low)
		log_printf(adap, file, "always keep input HPD high\n");
	else
		log_printf(adap, file,
			   "pull input HPD low if all output HPDs are low\n");
	if (vendor_id)
		log_printf(adap, file,
			   "splitter vendor ID: 0x%06x\n", vendor_id);
	if (manufacturer_name[0])
		log_printf(adap, file, "splitter manufacturer name: %s\n",
			   manufacturer_name);
	log_printf(adap, file, "splitter power status: %s\n",
		   pwr_state[extron->splitter.is_standby]);
	log_printf(adap, file, "%s port: %d (%s)\n",
		   port->is_input ? "input" : "output",
		   port->port.port, port->name);
	log_printf(adap, file, "splitter input port:\n");
	extron_adap_status_port(extron->ports[extron->num_out_ports], file);

	log_printf(adap, file, "splitter output ports:\n");
	for (i = 0; i < extron->num_out_ports; i++)
		extron_adap_status_port(extron->ports[i], file);

	if (!port->has_edid || !port->read_edid)
		return;

	for (i = 0; i < port->edid_blocks * 128; i += 16) {
		if (i % 128 == 0)
			log_printf(adap, file, "\n");
		log_printf(adap, file, "EDID: %*ph\n", 16, port->edid + i);
	}
}

static const struct cec_adap_ops extron_cec_adap_ops = {
	.adap_enable = extron_cec_adap_enable,
	.adap_log_addr = extron_cec_adap_log_addr,
	.adap_transmit = extron_cec_adap_transmit,
	.adap_nb_transmit_canceled = extron_cec_adap_nb_transmit_canceled,
	.adap_unconfigured = extron_cec_adap_unconfigured,
	.adap_status = extron_adap_status,
	.configured = extron_cec_configured,
	.received = extron_received,
};

static int extron_querycap(struct file *file, void *priv,
			   struct v4l2_capability *cap)
{
	struct extron_port *port = video_drvdata(file);

	strscpy(cap->driver, "extron-da-hd-4k-plus-cec", sizeof(cap->driver));
	strscpy(cap->card, cap->driver, sizeof(cap->card));
	snprintf(cap->bus_info, sizeof(cap->bus_info), "serio:%s", port->name);
	return 0;
}

static int extron_enum_input(struct file *file, void *priv, struct v4l2_input *inp)
{
	struct extron_port *port = video_drvdata(file);

	if (inp->index)
		return -EINVAL;
	inp->type = V4L2_INPUT_TYPE_CAMERA;
	snprintf(inp->name, sizeof(inp->name), "HDMI IN %u", port->port.port);
	inp->status = v4l2_ctrl_g_ctrl(port->ctrl_rx_power_present) ?
		0 : V4L2_IN_ST_NO_SIGNAL;
	return 0;
}

static int extron_g_input(struct file *file, void *priv, unsigned int *i)
{
	*i = 0;
	return 0;
}

static int extron_s_input(struct file *file, void *priv, unsigned int i)
{
	return i ? -EINVAL : 0;
}

static int extron_enum_output(struct file *file, void *priv, struct v4l2_output *out)
{
	struct extron_port *port = video_drvdata(file);

	if (out->index)
		return -EINVAL;
	out->type = V4L2_OUTPUT_TYPE_ANALOG;
	snprintf(out->name, sizeof(out->name), "HDMI OUT %u", port->port.port);
	return 0;
}

static int extron_g_output(struct file *file, void *priv, unsigned int *o)
{
	*o = 0;
	return 0;
}

static int extron_s_output(struct file *file, void *priv, unsigned int o)
{
	return o ? -EINVAL : 0;
}

static int extron_g_edid(struct file *file, void *_fh,
			 struct v4l2_edid *edid)
{
	struct extron_port *port = video_drvdata(file);

	memset(edid->reserved, 0, sizeof(edid->reserved));
	if (port->disconnected)
		return -ENODEV;
	if (edid->pad)
		return -EINVAL;
	if (!port->has_edid)
		return -ENODATA;
	if (!port->read_edid)
		extron_read_edid(port);
	if (!port->read_edid)
		return -ENODATA;
	if (edid->start_block == 0 && edid->blocks == 0) {
		edid->blocks = port->edid_blocks;
		return 0;
	}
	if (edid->start_block >= port->edid_blocks)
		return -EINVAL;
	if (edid->blocks > port->edid_blocks - edid->start_block)
		edid->blocks = port->edid_blocks - edid->start_block;
	memcpy(edid->edid, port->edid + edid->start_block * 128, edid->blocks * 128);
	return 0;
}

static int extron_s_edid(struct file *file, void *_fh, struct v4l2_edid *edid)
{
	struct extron_port *port = video_drvdata(file);

	memset(edid->reserved, 0, sizeof(edid->reserved));
	if (port->disconnected)
		return -ENODEV;
	if (edid->pad)
		return -EINVAL;

	/* Unfortunately it is not possible to clear the EDID */
	if (edid->blocks == 0)
		return -EINVAL;

	if (edid->blocks > MAX_EDID_BLOCKS) {
		edid->blocks = MAX_EDID_BLOCKS;
		return -E2BIG;
	}

	if (cec_get_edid_spa_location(edid->edid, edid->blocks * 128))
		v4l2_set_edid_phys_addr(edid->edid, edid->blocks * 128, 0);
	extron_parse_edid(port);
	return extron_write_edid(port, edid->edid, edid->blocks);
}

static int extron_log_status(struct file *file, void *priv)
{
	struct extron_port *port = video_drvdata(file);

	extron_adap_status(port->adap, NULL);
	return v4l2_ctrl_log_status(file, priv);
}

static const struct v4l2_ioctl_ops extron_ioctl_ops = {
	.vidioc_querycap		= extron_querycap,
	.vidioc_enum_input		= extron_enum_input,
	.vidioc_g_input			= extron_g_input,
	.vidioc_s_input			= extron_s_input,
	.vidioc_enum_output		= extron_enum_output,
	.vidioc_g_output		= extron_g_output,
	.vidioc_s_output		= extron_s_output,
	.vidioc_g_edid			= extron_g_edid,
	.vidioc_s_edid			= extron_s_edid,
	.vidioc_log_status		= extron_log_status,
	.vidioc_subscribe_event		= v4l2_ctrl_subscribe_event,
	.vidioc_unsubscribe_event	= v4l2_event_unsubscribe,
};

static const struct v4l2_file_operations extron_fops = {
	.owner		= THIS_MODULE,
	.open		= v4l2_fh_open,
	.release	= v4l2_fh_release,
	.poll		= v4l2_ctrl_poll,
	.unlocked_ioctl	= video_ioctl2,
};

static const struct video_device extron_videodev = {
	.name		= "extron-da-hd-4k-plus-cec",
	.vfl_dir	= VFL_DIR_RX,
	.fops		= &extron_fops,
	.ioctl_ops	= &extron_ioctl_ops,
	.minor		= -1,
	.release	= video_device_release_empty,
};

static void extron_disconnect(struct serio *serio)
{
	struct extron *extron = serio_get_drvdata(serio);
	unsigned int p;

	kthread_stop(extron->kthread_setup);

	for (p = 0; p < extron->num_ports; p++) {
		struct extron_port *port = extron->ports[p];

		if (!port)
			continue;
		port->disconnected = true;
		cancel_work_sync(&port->irq_work);
	}
	cancel_delayed_work_sync(&extron->work_update_edid);
	for (p = 0; p < extron->num_ports; p++) {
		struct extron_port *port = extron->ports[p];

		if (!port)
			continue;

		if (port->cec_was_registered) {
			if (cec_is_registered(port->adap))
				cec_unregister_adapter(port->adap);
			/*
			 * After registering the adapter, the
			 * extron_setup_thread() function took an extra
			 * reference to the device. We call the corresponding
			 * put here.
			 */
			cec_put_device(port->adap);
		} else {
			cec_delete_adapter(port->adap);
		}
		video_unregister_device(&port->vdev);
	}

	complete(&extron->edid_completion);

	for (p = 0; p < extron->num_ports; p++) {
		struct extron_port *port = extron->ports[p];

		if (!port)
			continue;
		v4l2_ctrl_handler_free(&port->hdl);
		mutex_destroy(&port->video_lock);
		kfree(port);
	}
	mutex_destroy(&extron->edid_lock);
	mutex_destroy(&extron->serio_lock);
	extron->serio = NULL;
	serio_set_drvdata(serio, NULL);
	serio_close(serio);
}

static int extron_setup(struct extron *extron)
{
	struct serio *serio = extron->serio;
	struct extron_port *port;
	u8 *reply = extron->reply;
	unsigned int p;
	unsigned int major, minor;
	int err;

	/*
	 * Attempt to disable CEC: avoid received CEC messages
	 * from interfering with the other serial port traffic.
	 */
	extron_send_and_wait(extron, NULL, "WI1*0CCEC", NULL);
	extron_send_and_wait(extron, NULL, "WO0*CCEC", NULL);

	/* Obtain unit part number */
	err = extron_send_and_wait(extron, NULL, "N", "Pno");
	if (err)
		return err;
	dev_info(extron->dev, "Unit part number: %s\n", reply + 3);
	if (strcmp(reply + 3, "60-1607-01") &&
	    strcmp(reply + 3, "60-1608-01") &&
	    strcmp(reply + 3, "60-1609-01")) {
		dev_err(extron->dev, "Unsupported model\n");
		return -ENODEV;
	}
	/* Up to 6 output ports and one input port */
	extron->num_out_ports = 2 * (reply[9] - '6');
	extron->splitter.num_out_ports = extron->num_out_ports;
	extron->splitter.ports = extron->splitter_ports;
	extron->splitter.dev = extron->dev;
	extron->num_in_ports = 1;
	extron->num_ports = extron->num_out_ports + extron->num_in_ports;
	dev_info(extron->dev, "Unit output ports: %d\n", extron->num_out_ports);
	dev_info(extron->dev, "Unit input ports: %d\n", extron->num_in_ports);

	err = extron_send_and_wait(extron, NULL, "W CN", "Ipn ");
	if (err)
		return err;
	dev_info(extron->dev, "Unit name: %s\n", reply + 4);
	strscpy(extron->unit_name, reply + 4, sizeof(extron->unit_name));

	err = extron_send_and_wait(extron, NULL, "*Q", "Bld");
	if (err)
		return err;
	dev_info(extron->dev, "Unit FW Version: %s\n", reply + 3);
	strscpy(extron->unit_fw_version, reply + 3,
		sizeof(extron->unit_fw_version));
	if (sscanf(reply + 3, "%u.%u.", &major, &minor) < 2 ||
	    major < 1 || minor < 2) {
		dev_err(extron->dev,
			"Unsupported FW version (only 1.02 or up is supported)\n");
		return -ENODEV;
	}

	err = extron_send_and_wait(extron, NULL, "2i", "Inf02*");
	if (err)
		return err;
	dev_info(extron->dev, "Unit Type: %s\n", reply + 6);
	strscpy(extron->unit_type, reply + 6, sizeof(extron->unit_type));

	err = extron_send_and_wait(extron, NULL, "39Q", "Ver39*");
	if (err)
		return err;
	dev_info(extron->dev, "CEC Engine Version: %s\n", reply + 6);
	strscpy(extron->unit_cec_engine_version, reply + 6,
		sizeof(extron->unit_cec_engine_version));

	/* Disable CEC */
	err = extron_send_and_wait(extron, NULL, "WI1*0CCEC", "CcecI1*");
	if (err)
		return err;
	err = extron_send_and_wait(extron, NULL, "WO0*CCEC", "CcecO0");
	if (err)
		return err;

	extron->hpd_never_low = hpd_never_low;

	/* Pull input port HPD low if all output ports also have a low HPD */
	if (hpd_never_low) {
		dev_info(extron->dev, "Always keep input HPD high\n");
	} else {
		dev_info(extron->dev, "Pull input HPD low if all output HPDs are low\n");
		extron_send_and_wait(extron, NULL, "W1ihpd", "Ihpd1");
	}

	for (p = 0; p < extron->num_ports; p++) {
		u32 caps = CEC_CAP_DEFAULTS | CEC_CAP_MONITOR_ALL;

		if (vendor_id)
			caps &= ~CEC_CAP_LOG_ADDRS;
		port = kzalloc(sizeof(*port), GFP_KERNEL);
		if (!port)
			return -ENOMEM;

		INIT_WORK(&port->irq_work, extron_irq_work_handler);
		spin_lock_init(&port->msg_lock);
		mutex_init(&port->video_lock);
		port->extron = extron;
		port->is_input = p >= extron->num_out_ports;
		port->direction = port->is_input ? 'I' : 'O';
		port->port.port = 1 + (port->is_input ? p - extron->num_out_ports : p);
		port->port.splitter = &extron->splitter;
		port->phys_addr = CEC_PHYS_ADDR_INVALID;
		snprintf(port->name, sizeof(port->name), "%s-%s-%u",
			 dev_name(&serio->dev), port->is_input ? "in" : "out",
			 port->port.port);

		port->dev = extron->dev;
		port->adap = cec_allocate_adapter(&extron_cec_adap_ops, port,
						  port->name, caps, 1);
		err = PTR_ERR_OR_ZERO(port->adap);
		if (err < 0) {
			kfree(port);
			return err;
		}

		port->adap->xfer_timeout_ms = EXTRON_TIMEOUT_SECS * 1000;
		port->port.adap = port->adap;
		port->vdev = extron_videodev;
		port->vdev.lock = &port->video_lock;
		port->vdev.v4l2_dev = &extron->v4l2_dev;
		port->vdev.ctrl_handler = &port->hdl;
		port->vdev.device_caps = V4L2_CAP_EDID;
		video_set_drvdata(&port->vdev, port);

		v4l2_ctrl_handler_init(&port->hdl, 2);

		if (port->is_input) {
			port->vdev.vfl_dir = VFL_DIR_RX;
			port->ctrl_rx_power_present =
				v4l2_ctrl_new_std(&port->hdl, NULL,
						  V4L2_CID_DV_RX_POWER_PRESENT,
						  0, 1, 0, 0);
			port->has_edid = true;
		} else {
			port->vdev.vfl_dir = VFL_DIR_TX;
			port->ctrl_tx_hotplug =
				v4l2_ctrl_new_std(&port->hdl, NULL,
						  V4L2_CID_DV_TX_HOTPLUG,
						  0, 1, 0, 0);
			port->ctrl_tx_edid_present =
				v4l2_ctrl_new_std(&port->hdl, NULL,
						  V4L2_CID_DV_TX_EDID_PRESENT,
						  0, 1, 0, 0);
		}

		err = port->hdl.error;
		if (err < 0) {
			cec_delete_adapter(port->adap);
			kfree(port);
			return err;
		}
		extron->ports[p] = port;
		extron->splitter_ports[p] = &port->port;
		if (port->is_input && manufacturer_name[0])
			extron_write_edid(port, hdmi_edid, 2);
	}

	/* Enable CEC (manual mode, i.e. controlled by the driver) */
	err = extron_send_and_wait(extron, NULL, "WI1*20CCEC", "CcecI1*");
	if (err)
		return err;

	err = extron_send_and_wait(extron, NULL, "WO20*CCEC", "CcecO20");
	if (err)
		return err;

	/* Set logical addresses to 15 */
	err = extron_send_and_wait(extron, NULL, "WI1*15LCEC", "LcecI1*15");
	if (err)
		return err;

	for (p = 0; p < extron->num_out_ports; p++) {
		char cmd[20];
		char resp[20];

		snprintf(cmd, sizeof(cmd), "WO%u*15LCEC", p + 1);
		snprintf(resp, sizeof(resp), "LcecO%u*15", p + 1);
		err = extron_send_and_wait(extron, extron->ports[p], cmd, resp);
		if (err)
			return err;
	}

	/*
	 * The Extron is now ready for operation. Specifically it is now
	 * possible to retrieve EDIDs.
	 */
	extron->is_ready = true;

	/* Query HDCP and Signal states, used to update the initial state */
	err = extron_send_and_wait(extron, NULL, "WHDCP", "Hdcp");
	if (err)
		return err;

	return extron_send_and_wait(extron, NULL, "WLS", "Sig");
}

static int extron_setup_thread(void *_extron)
{
	struct extron *extron = _extron;
	struct extron_port *port;
	unsigned int p;
	bool poll_splitter = false;
	bool was_connected = true;
	int err;

	while (1) {
		if (kthread_should_stop())
			return 0;
		err = extron_send_and_wait(extron, NULL, "W3CV", "Vrb3");
		// that should make it possible to detect a serio disconnect
		// here by stopping the workqueue
		if (err >= 0)
			break;
		was_connected = false;
		ssleep(1);
	}

	/*
	 * If the Extron was not connected at probe() time, i.e. it just got
	 * powered up and while the serial port is working, the firmware is
	 * still booting up, then wait 10 seconds for the firmware to settle.
	 *
	 * Trying to continue too soon means that some commands will not
	 * work yet.
	 */
	if (!was_connected)
		ssleep(10);

	err = extron_setup(extron);
	if (err)
		goto disable_ports;

	for (p = 0; p < extron->num_ports; p++) {
		struct cec_log_addrs log_addrs = {};

		port = extron->ports[p];
		if (port->is_input && manufacturer_name[0])
			v4l2_disable_ioctl(&port->vdev, VIDIOC_S_EDID);
		err = video_register_device(&port->vdev, VFL_TYPE_VIDEO, -1);
		if (err) {
			v4l2_err(&extron->v4l2_dev, "Failed to register video device\n");
			goto disable_ports;
		}

		err = cec_register_adapter(port->adap, extron->dev);
		if (err < 0)
			goto disable_ports;
		port->dev = &port->adap->devnode.dev;
		port->cec_was_registered = true;
		/*
		 * This driver is unusual in that the whole setup takes place
		 * in a thread since it can take such a long time before the
		 * Extron Splitter boots up, and you do not want to block the
		 * probe function on this driver. In addition, as soon as
		 * CEC adapters come online, they can be used, and you cannot
		 * just unregister them again if an error occurs, since that
		 * can delete the underlying CEC adapter, which might already
		 * be in use.
		 *
		 * So we take an additional reference to the adapter. This
		 * allows us to unregister the device node if needed, without
		 * deleting the actual adapter.
		 *
		 * In the disconnect function we will do the corresponding
		 * put call to ensure the adapter is deleted.
		 */
		cec_get_device(port->adap);

		/*
		 * If vendor_id wasn't set, then userspace configures the
		 * CEC devices. Otherwise the driver configures the CEC
		 * devices as TV (input) and Playback (outputs) devices
		 * and the driver processes all CEC messages.
		 */
		if (!vendor_id)
			continue;

		log_addrs.cec_version = CEC_OP_CEC_VERSION_2_0;
		log_addrs.num_log_addrs = 1;
		log_addrs.vendor_id = vendor_id;
		if (port->is_input) {
			strscpy(log_addrs.osd_name, "Splitter In",
				sizeof(log_addrs.osd_name));
			log_addrs.log_addr_type[0] = CEC_LOG_ADDR_TYPE_TV;
			log_addrs.primary_device_type[0] = CEC_OP_PRIM_DEVTYPE_TV;
			log_addrs.all_device_types[0] = CEC_OP_ALL_DEVTYPE_TV;
		} else {
			snprintf(log_addrs.osd_name, sizeof(log_addrs.osd_name),
				 "Splitter Out%u", port->port.port);
			log_addrs.log_addr_type[0] = CEC_LOG_ADDR_TYPE_PLAYBACK;
			log_addrs.primary_device_type[0] = CEC_OP_PRIM_DEVTYPE_PLAYBACK;
			log_addrs.all_device_types[0] = CEC_OP_ALL_DEVTYPE_PLAYBACK;
		}
		err = cec_s_log_addrs(port->adap, &log_addrs, false);
		if (err < 0)
			goto disable_ports;
	}
	poll_splitter = true;

	port = extron->ports[extron->num_out_ports];
	while (!kthread_should_stop()) {
		ssleep(1);
		if (hpd_never_low != extron->hpd_never_low) {
			/*
			 * Keep input port HPD high at all times, or pull it low
			 * if all output ports also have a low HPD
			 */
			if (hpd_never_low) {
				dev_info(extron->dev, "Always keep input HPD high\n");
				extron_send_and_wait(extron, NULL, "W0ihpd", "Ihpd0");
			} else {
				dev_info(extron->dev, "Pull input HPD low if all output HPDs are low\n");
				extron_send_and_wait(extron, NULL, "W1ihpd", "Ihpd1");
			}
			extron->hpd_never_low = hpd_never_low;
		}
		if (poll_splitter &&
		    cec_splitter_poll(&extron->splitter, port->adap, debug) &&
		    manufacturer_name[0]) {
			/*
			 * Sinks were lost, so see if the input edid needs to
			 * be updated.
			 */
			cancel_delayed_work_sync(&extron->work_update_edid);
			schedule_delayed_work(&extron->work_update_edid,
					      msecs_to_jiffies(1000));
		}
	}
	return 0;

disable_ports:
	extron->is_ready = false;
	for (p = 0; p < extron->num_ports; p++) {
		struct extron_port *port = extron->ports[p];

		if (!port)
			continue;
		port->disconnected = true;
		cancel_work_sync(&port->irq_work);
		video_unregister_device(&port->vdev);
		if (port->cec_was_registered)
			cec_unregister_adapter(port->adap);
	}
	cancel_delayed_work_sync(&extron->work_update_edid);
	complete(&extron->edid_completion);
	dev_err(extron->dev, "Setup failed with error %d\n", err);
	while (!kthread_should_stop())
		ssleep(1);
	return err;
}

static int extron_connect(struct serio *serio, struct serio_driver *drv)
{
	struct extron *extron;
	int err = -ENOMEM;

	if (manufacturer_name[0] &&
	    (!isupper(manufacturer_name[0]) ||
	     !isupper(manufacturer_name[1]) ||
	     !isupper(manufacturer_name[2]))) {
		dev_warn(&serio->dev, "ignoring invalid manufacturer name\n");
		manufacturer_name[0] = 0;
	}

	extron = kzalloc(sizeof(*extron), GFP_KERNEL);

	if (!extron)
		return -ENOMEM;

	extron->serio = serio;
	extron->dev = &serio->dev;
	mutex_init(&extron->serio_lock);
	mutex_init(&extron->edid_lock);
	INIT_DELAYED_WORK(&extron->work_update_edid, update_edid_work);

	err = v4l2_device_register(extron->dev, &extron->v4l2_dev);
	if (err)
		goto free_device;

	err = serio_open(serio, drv);
	if (err)
		goto unreg_v4l2_dev;

	serio_set_drvdata(serio, extron);
	init_completion(&extron->edid_completion);

	extron->kthread_setup = kthread_run(extron_setup_thread, extron,
					    "extron-da-hd-4k-plus-cec-%s", dev_name(&serio->dev));
	if (!IS_ERR(extron->kthread_setup))
		return 0;

	dev_err(extron->dev, "kthread_run() failed\n");
	err = PTR_ERR(extron->kthread_setup);

	extron->serio = NULL;
	serio_set_drvdata(serio, NULL);
	serio_close(serio);
unreg_v4l2_dev:
	v4l2_device_unregister(&extron->v4l2_dev);
free_device:
	mutex_destroy(&extron->edid_lock);
	mutex_destroy(&extron->serio_lock);
	kfree(extron);
	return err;
}

static const struct serio_device_id extron_serio_ids[] = {
	{
		.type	= SERIO_RS232,
		.proto	= SERIO_EXTRON_DA_HD_4K_PLUS,
		.id	= SERIO_ANY,
		.extra	= SERIO_ANY,
	},
	{ 0 }
};

MODULE_DEVICE_TABLE(serio, extron_serio_ids);

static struct serio_driver extron_drv = {
	.driver		= {
		.name	= "extron-da-hd-4k-plus-cec",
	},
	.description	= "Extron DA HD 4K PLUS HDMI CEC driver",
	.id_table	= extron_serio_ids,
	.interrupt	= extron_interrupt,
	.connect	= extron_connect,
	.disconnect	= extron_disconnect,
};

module_serio_driver(extron_drv);
