// SPDX-License-Identifier: (LGPL-2.1-only OR BSD-3-Clause)
/*
 * CEC common helper functions
 *
 * Copyright 2017 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
 */

#include <cctype>
#include <string>

#include <dirent.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>

#include <cec-info.h>
#include <cec-htng.h>

#include "cec-msgs-gen.h"

const char *cec_opcode2s(unsigned opcode)
{
	for (auto i : msgtable)
		if (i.opcode == opcode)
			return i.name;
	return nullptr;
}

const char *cec_cdc_opcode2s(unsigned cdc_opcode)
{
	for (auto i : cdcmsgtable)
		if (i.opcode == cdc_opcode)
			return i.name;
	return nullptr;
}

const char *cec_htng_opcode2s(unsigned htng_opcode)
{
	for (auto i : htngmsgtable)
		if (i.opcode == htng_opcode)
			return i.name;
	return nullptr;
}

static std::string caps2s(unsigned caps)
{
	std::string s;

	if (caps & CEC_CAP_PHYS_ADDR)
		s += "\t\tPhysical Address\n";
	if (caps & CEC_CAP_LOG_ADDRS)
		s += "\t\tLogical Addresses\n";
	if (caps & CEC_CAP_TRANSMIT)
		s += "\t\tTransmit\n";
	if (caps & CEC_CAP_PASSTHROUGH)
		s += "\t\tPassthrough\n";
	if (caps & CEC_CAP_RC)
		s += "\t\tRemote Control Support\n";
	if (caps & CEC_CAP_MONITOR_ALL)
		s += "\t\tMonitor All\n";
	if (caps & CEC_CAP_NEEDS_HPD)
		s += "\t\tNeeds HPD\n";
	if (caps & CEC_CAP_MONITOR_PIN)
		s += "\t\tMonitor Pin\n";
	if (caps & CEC_CAP_CONNECTOR_INFO)
		s += "\t\tConnector Info\n";
	return s;
}

static std::string laflags2s(unsigned flags)
{
	std::string s;

	if (!flags)
		return s;

	s = "(";
	if (flags & CEC_LOG_ADDRS_FL_ALLOW_UNREG_FALLBACK)
		s += "Allow Fallback to Unregistered, ";
	if (flags & CEC_LOG_ADDRS_FL_ALLOW_RC_PASSTHRU)
		s += "Allow RC Passthrough, ";
	if (flags & CEC_LOG_ADDRS_FL_CDC_ONLY)
		s += "CDC-Only, ";
	if (s.length())
		s.erase(s.length() - 2, 2);
	return s + ")";
}

const char *cec_version2s(unsigned version)
{
	switch (version) {
	case CEC_OP_CEC_VERSION_1_3A:
		return "1.3a";
	case CEC_OP_CEC_VERSION_1_4:
		return "1.4";
	case CEC_OP_CEC_VERSION_2_0:
		return "2.0";
	default:
		return "Unknown";
	}
}

/*
 * Most of these vendor IDs come from include/cectypes.h from libcec.
 */
const char *cec_vendor2s(unsigned vendor)
{
	switch (vendor) {
	case 0x000039:
	case 0x000ce7:
		return "Toshiba";
	case 0x0000f0:
		return "Samsung";
	case 0x0005cd:
		return "Denon";
	case 0x000678:
		return "Marantz";
	case 0x000982:
		return "Loewe";
	case 0x0009b0:
		return "Onkyo";
	case 0x000c03:
		return "HDMI";
	case 0x001582:
		return "Pulse-Eight";
	case 0x001950:
	case 0x9c645e:
		return "Harman Kardon";
	case 0x001a11:
		return "Google";
	case 0x0020c7:
		return "Akai";
	case 0x002467:
		return "AOC";
	case 0x005060:
		return "Cisco";
	case 0x008045:
		return "Panasonic";
	case 0x00903e:
		return "Philips";
	case 0x009053:
		return "Daewoo";
	case 0x00a0de:
		return "Yamaha";
	case 0x00d0d5:
		return "Grundig";
	case 0x00d38d:
		return "Hospitality Profile";
	case 0x00e036:
		return "Pioneer";
	case 0x00e091:
		return "LG";
	case 0x08001f:
	case 0x534850:
		return "Sharp";
	case 0x080046:
		return "Sony";
	case 0x0acd8f:
		return "CDDC";
	case 0x18c086:
		return "Broadcom";
	case 0x5cad76:
		return "TCL";
	case 0x6b746d:
		return "Vizio";
	case 0x743a65:
		return "NEC";
	case 0x8065e9:
		return "Benq";
	default:
		return nullptr;
	}
}

const char *cec_prim_type2s(unsigned type)
{
	switch (type) {
	case CEC_OP_PRIM_DEVTYPE_TV:
		return "TV";
	case CEC_OP_PRIM_DEVTYPE_RECORD:
		return "Record";
	case CEC_OP_PRIM_DEVTYPE_TUNER:
		return "Tuner";
	case CEC_OP_PRIM_DEVTYPE_PLAYBACK:
		return "Playback";
	case CEC_OP_PRIM_DEVTYPE_AUDIOSYSTEM:
		return "Audio System";
	case CEC_OP_PRIM_DEVTYPE_SWITCH:
		return "Switch";
	case CEC_OP_PRIM_DEVTYPE_PROCESSOR:
		return "Processor";
	default:
		return "Unknown";
	}
}

const char *cec_la_type2s(unsigned type)
{
	switch (type) {
	case CEC_LOG_ADDR_TYPE_TV:
		return "TV";
	case CEC_LOG_ADDR_TYPE_RECORD:
		return "Record";
	case CEC_LOG_ADDR_TYPE_TUNER:
		return "Tuner";
	case CEC_LOG_ADDR_TYPE_PLAYBACK:
		return "Playback";
	case CEC_LOG_ADDR_TYPE_AUDIOSYSTEM:
		return "Audio System";
	case CEC_LOG_ADDR_TYPE_SPECIFIC:
		return "Specific";
	case CEC_LOG_ADDR_TYPE_UNREGISTERED:
		return "Unregistered";
	default:
		return "Unknown";
	}
}

const char *cec_la2s(unsigned la)
{
	switch (la & 0xf) {
	case 0:
		return "TV";
	case 1:
		return "Recording Device 1";
	case 2:
		return "Recording Device 2";
	case 3:
		return "Tuner 1";
	case 4:
		return "Playback Device 1";
	case 5:
		return "Audio System";
	case 6:
		return "Tuner 2";
	case 7:
		return "Tuner 3";
	case 8:
		return "Playback Device 2";
	case 9:
		return "Recording Device 3";
	case 10:
		return "Tuner 4";
	case 11:
		return "Playback Device 3";
	case 12:
		return "Backup 1";
	case 13:
		return "Backup 2";
	case 14:
		return "Specific";
	case 15:
	default:
		return "Unregistered";
	}
}

std::string cec_all_dev_types2s(unsigned types)
{
	std::string s;

	if (types & CEC_OP_ALL_DEVTYPE_TV)
		s += "TV, ";
	if (types & CEC_OP_ALL_DEVTYPE_RECORD)
		s += "Record, ";
	if (types & CEC_OP_ALL_DEVTYPE_TUNER)
		s += "Tuner, ";
	if (types & CEC_OP_ALL_DEVTYPE_PLAYBACK)
		s += "Playback, ";
	if (types & CEC_OP_ALL_DEVTYPE_AUDIOSYSTEM)
		s += "Audio System, ";
	if (types & CEC_OP_ALL_DEVTYPE_SWITCH)
		s += "Switch, ";
	if (s.length())
		return s.erase(s.length() - 2, 2);
	return s;
}

std::string cec_rc_src_prof2s(unsigned prof, const std::string &prefix)
{
	std::string s;

	prof &= 0x1f;
	if (prof == 0)
		return prefix + "\t\tNone\n";
	if (prof & CEC_OP_FEAT_RC_SRC_HAS_DEV_ROOT_MENU)
		s += prefix + "\t\tSource Has Device Root Menu\n";
	if (prof & CEC_OP_FEAT_RC_SRC_HAS_DEV_SETUP_MENU)
		s += prefix + "\t\tSource Has Device Setup Menu\n";
	if (prof & CEC_OP_FEAT_RC_SRC_HAS_MEDIA_CONTEXT_MENU)
		s += prefix + "\t\tSource Has Contents Menu\n";
	if (prof & CEC_OP_FEAT_RC_SRC_HAS_MEDIA_TOP_MENU)
		s += prefix + "\t\tSource Has Media Top Menu\n";
	if (prof & CEC_OP_FEAT_RC_SRC_HAS_MEDIA_CONTEXT_MENU)
		s += prefix + "\t\tSource Has Media Context-Sensitive Menu\n";
	return s;
}

std::string cec_dev_feat2s(unsigned feat, const std::string &prefix)
{
	std::string s;

	feat &= 0x7e;
	if (feat == 0)
		return prefix + "\t\tNone\n";
	if (feat & CEC_OP_FEAT_DEV_HAS_RECORD_TV_SCREEN)
		s += prefix + "\t\tTV Supports <Record TV Screen>\n";
	if (feat & CEC_OP_FEAT_DEV_HAS_SET_OSD_STRING)
		s += prefix + "\t\tTV Supports <Set OSD String>\n";
	if (feat & CEC_OP_FEAT_DEV_HAS_DECK_CONTROL)
		s += prefix + "\t\tSupports Deck Control\n";
	if (feat & CEC_OP_FEAT_DEV_HAS_SET_AUDIO_RATE)
		s += prefix + "\t\tSource Supports <Set Audio Rate>\n";
	if (feat & CEC_OP_FEAT_DEV_SINK_HAS_ARC_TX)
		s += prefix + "\t\tSink Supports ARC Tx\n";
	if (feat & CEC_OP_FEAT_DEV_SOURCE_HAS_ARC_RX)
		s += prefix + "\t\tSource Supports ARC Rx\n";
	return s;
}

static std::string tx_status2s(const struct cec_msg &msg)
{
	std::string s;
	char num[4];
	unsigned stat = msg.tx_status;

	if (stat)
		s += "Tx";
	if (stat & CEC_TX_STATUS_OK)
		s += ", OK";
	if (stat & CEC_TX_STATUS_ARB_LOST) {
		sprintf(num, "%u", msg.tx_arb_lost_cnt);
		s += ", Arbitration Lost";
		if (msg.tx_arb_lost_cnt)
			s += " (" + std::string(num) + ")";
	}
	if (stat & CEC_TX_STATUS_NACK) {
		sprintf(num, "%u", msg.tx_nack_cnt);
		s += ", Not Acknowledged";
		if (msg.tx_nack_cnt)
			s += " (" + std::string(num) + ")";
	}
	if (stat & CEC_TX_STATUS_LOW_DRIVE) {
		sprintf(num, "%u", msg.tx_low_drive_cnt);
		s += ", Low Drive";
		if (msg.tx_low_drive_cnt)
			s += " (" + std::string(num) + ")";
	}
	if (stat & CEC_TX_STATUS_ERROR) {
		sprintf(num, "%u", msg.tx_error_cnt);
		s += ", Error";
		if (msg.tx_error_cnt)
			s += " (" + std::string(num) + ")";
	}
	if (stat & CEC_TX_STATUS_ABORTED)
		s += ", Aborted";
	if (stat & CEC_TX_STATUS_TIMEOUT)
		s += ", Timeout";
	if (stat & CEC_TX_STATUS_MAX_RETRIES)
		s += ", Max Retries";
	return s;
}

static std::string rx_status2s(unsigned stat)
{
	std::string s;

	if (stat)
		s += "Rx";
	if (stat & CEC_RX_STATUS_OK)
		s += ", OK";
	if (stat & CEC_RX_STATUS_TIMEOUT)
		s += ", Timeout";
	if (stat & CEC_RX_STATUS_FEATURE_ABORT)
		s += ", Feature Abort";
	if (stat & CEC_RX_STATUS_ABORTED)
		s += ", Aborted";
	return s;
}

std::string cec_status2s(const struct cec_msg &msg)
{
	std::string s;

	if (msg.tx_status)
		s = tx_status2s(msg);
	if (msg.rx_status) {
		if (!s.empty())
			s += ", ";
		s += rx_status2s(msg.rx_status);
	}
	return s;
}

void cec_driver_info(const struct cec_caps &caps,
		     const struct cec_log_addrs &laddrs, __u16 phys_addr,
		     const struct cec_connector_info &conn_info)
{
	printf("Driver Info:\n");
	printf("\tDriver Name                : %s\n", caps.driver);
	printf("\tAdapter Name               : %s\n", caps.name);
	printf("\tCapabilities               : 0x%08x\n", caps.capabilities);
	printf("%s", caps2s(caps.capabilities).c_str());
	printf("\tDriver version             : %d.%d.%d\n",
			caps.version >> 16,
			(caps.version >> 8) & 0xff,
			caps.version & 0xff);
	printf("\tAvailable Logical Addresses: %u\n",
	       caps.available_log_addrs);
	switch (conn_info.type) {
	case CEC_CONNECTOR_TYPE_NO_CONNECTOR:
		printf("\tConnector Info             : None\n");
		break;
	case CEC_CONNECTOR_TYPE_DRM:
		printf("\tDRM Connector Info         : card %u, connector %u\n",
		       conn_info.drm.card_no, conn_info.drm.connector_id);
		break;
	default:
		printf("\tConnector Info             : Type %u\n",
		       conn_info.type);
		break;
	}

	printf("\tPhysical Address           : %x.%x.%x.%x\n",
	       cec_phys_addr_exp(phys_addr));
	printf("\tLogical Address Mask       : 0x%04x\n", laddrs.log_addr_mask);
	printf("\tCEC Version                : %s\n", cec_version2s(laddrs.cec_version));
	if (laddrs.vendor_id != CEC_VENDOR_ID_NONE) {
		const char *vendor = cec_vendor2s(laddrs.vendor_id);

		if (vendor)
			printf("\tVendor ID                  : 0x%06x (%s)\n",
			       laddrs.vendor_id, vendor);
		else
			printf("\tVendor ID                  : 0x%06x, %u\n",
			       laddrs.vendor_id, laddrs.vendor_id);
	}
	printf("\tOSD Name                   : '%s'\n", laddrs.osd_name);
	printf("\tLogical Addresses          : %u %s\n",
	       laddrs.num_log_addrs, laflags2s(laddrs.flags).c_str());
	for (unsigned i = 0; i < laddrs.num_log_addrs; i++) {
		if (laddrs.log_addr[i] == CEC_LOG_ADDR_INVALID)
			printf("\n\t  Logical Address          : Not Allocated\n");
		else
			printf("\n\t  Logical Address          : %d (%s)\n",
			       laddrs.log_addr[i], cec_la2s(laddrs.log_addr[i]));
		printf("\t    Primary Device Type    : %s\n",
		       cec_prim_type2s(laddrs.primary_device_type[i]));
		printf("\t    Logical Address Type   : %s\n",
		       cec_la_type2s(laddrs.log_addr_type[i]));
		printf("\t    All Device Types       : %s\n",
		       cec_all_dev_types2s(laddrs.all_device_types[i]).c_str());

		bool is_dev_feat = false;
		for (__u8 byte : laddrs.features[i]) {
				if (!is_dev_feat) {
				if (byte & 0x40) {
					printf("\t    RC Source Profile      :\n%s",
					       cec_rc_src_prof2s(byte, "").c_str());
				} else {
					const char *s = "Reserved";

					switch (byte & 0xf) {
					case 0:
						s = "None";
						break;
					case 2:
						s = "RC Profile 1";
						break;
					case 6:
						s = "RC Profile 2";
						break;
					case 10:
						s = "RC Profile 3";
						break;
					case 14:
						s = "RC Profile 4";
						break;
					}
					printf("\t    RC TV Profile          : %s\n", s);
				}
			} else {
				printf("\t    Device Features        :\n%s",
				       cec_dev_feat2s(byte, "").c_str());
			}
			if (byte & CEC_OP_FEAT_EXT)
				continue;
			if (!is_dev_feat)
				is_dev_feat = true;
			else
				break;
		}
	}
}

std::string cec_device_find(const char *driver, const char *adapter)
{
	DIR *dp;
	struct dirent *ep;
	std::string name;

	dp = opendir("/dev");
	if (dp == nullptr) {
		perror("Couldn't open the directory");
		return name;
	}
	while ((ep = readdir(dp)))
		if (!memcmp(ep->d_name, "cec", 3) && isdigit(ep->d_name[3])) {
			std::string devname("/dev/");
			struct cec_caps caps;
			int fd;

			devname += ep->d_name;
			fd = open(devname.c_str(), O_RDWR);

			if (fd < 0)
				continue;
			int err = ioctl(fd, CEC_ADAP_G_CAPS, &caps);
			close(fd);
			if (err)
				continue;
			if ((!driver || !strcmp(driver, caps.driver)) &&
			    (!adapter || !strcmp(adapter, caps.name))) {
				name = devname;
				break;
			}
		}
	closedir(dp);
	return name;
}
