// SPDX-License-Identifier: (GPL-2.0-only OR BSD-3-Clause)
/*
 * Copyright 2016 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
 */

#include <cerrno>
#include <ctime>
#include <string>

#include <sys/ioctl.h>
#include <sys/time.h>

#include "cec-follower.h"
#include "compiler.h"

#define VOLUME_MAX 0x64
#define VOLUME_MIN 0

/* States for the RC handling */
#define NOPRESS 0
#define PRESS 1
#define PRESS_HOLD 2

/* The follower safety timeout as defined in the spec */
#define FOLLOWER_SAFETY_TIMEOUT 450
#define MIN_INITIATOR_REP_TIME 200
#define MAX_INITIATOR_REP_TIME 500

/* Time between each polling message sent to a device */
#define POLL_PERIOD 15000

/* The maximum interval in nanoseconds between audio rate messages as defined in the spec */
#define MAX_AUD_RATE_MSG_INTERVAL_NS (2 * 1000000000ULL)

/* The maximum interval in nanoseconds to allow a deck to skip forward/reverse */
#define MAX_DECK_SKIP_NS (2 * 1000000000ULL)

struct cec_enum_values {
	const char *type_name;
	__u8 value;
};

struct la_info la_info[15];

static struct timespec start_monotonic;
static struct timeval start_timeofday;
static constexpr time_t time_to_transient = 1;
static constexpr time_t time_to_stable = 8;

static const char *get_ui_cmd_string(__u8 ui_cmd)
{
	return cec_log_ui_cmd_string(ui_cmd) ? : "Unknown";
}

static std::string ts2s(__u64 ts, bool wallclock)
{
	std::string s;
	struct timeval sub;
	struct timeval res;
	__u64 diff;
	char buf[64];
	time_t t;

	if (!wallclock) {
		sprintf(buf, "%llu.%03llus", ts / 1000000000, (ts % 1000000000) / 1000000);
		return buf;
	}
	diff = ts - start_monotonic.tv_sec * 1000000000ULL - start_monotonic.tv_nsec;
	sub.tv_sec = diff / 1000000000ULL;
	sub.tv_usec = (diff % 1000000000ULL) / 1000;
	timeradd(&start_timeofday, &sub, &res);
	t = res.tv_sec;
	s = ctime(&t);
	s = s.substr(0, s.length() - 6);
	sprintf(buf, "%03lu", res.tv_usec / 1000);
	return s + "." + buf;
}

static void log_event(struct cec_event &ev, bool wallclock)
{
	__u16 pa;

	if (ev.flags & CEC_EVENT_FL_DROPPED_EVENTS)
		printf("(Note: events were lost)\n");
	if (ev.flags & CEC_EVENT_FL_INITIAL_STATE)
		printf("Initial ");
	switch (ev.event) {
	case CEC_EVENT_STATE_CHANGE:
		pa = ev.state_change.phys_addr;
		printf("Event: State Change: PA: %x.%x.%x.%x, LA mask: 0x%04x\n",
		       cec_phys_addr_exp(pa),
		       ev.state_change.log_addr_mask);
		break;
	case CEC_EVENT_LOST_MSGS:
		printf("Event: Lost Messages\n");
		break;
	case CEC_EVENT_PIN_HPD_LOW:
	case CEC_EVENT_PIN_HPD_HIGH:
		printf("Event: HPD Pin %s\n",
		       ev.event == CEC_EVENT_PIN_HPD_HIGH ? "High" : "Low");
		warn("Unexpected HPD pin event!\n");
		break;
	case CEC_EVENT_PIN_CEC_LOW:
	case CEC_EVENT_PIN_CEC_HIGH:
		printf("Event: CEC Pin %s\n",
		       ev.event == CEC_EVENT_PIN_CEC_HIGH ? "High" : "Low");
		warn("Unexpected CEC pin event!\n");
		break;
	default:
		printf("Event: Unknown (0x%x)\n", ev.event);
		break;
	}
	if (show_info)
		printf("\tTimestamp: %s\n", ts2s(ev.ts, wallclock).c_str());
}

void reply_feature_abort(struct node *node, struct cec_msg *msg, __u8 reason)
{
	unsigned la = cec_msg_initiator(msg);
	__u8 opcode = cec_msg_opcode(msg);
	__u64 ts_now = get_ts();

	if (cec_msg_is_broadcast(msg) || cec_msg_initiator(msg) == CEC_LOG_ADDR_UNREGISTERED)
		return;
	if (reason == CEC_OP_ABORT_UNRECOGNIZED_OP) {
		la_info[la].feature_aborted[opcode].count++;
		if (la_info[la].feature_aborted[opcode].count == 2) {
			/* If the Abort Reason was "Unrecognized opcode", the Initiator should not send
			   the same message to the same Follower again at that time to avoid saturating
			   the bus. */
			warn("Received message %s from LA %d (%s) shortly after\n",
			     opcode2s(msg).c_str(), la, cec_la2s(la));
			warn("replying Feature Abort [Unrecognized Opcode] to the same message.\n");
		}
	}
	else if (la_info[la].feature_aborted[opcode].count) {
		warn("Replying Feature Abort with abort reason different than [Unrecognized Opcode]\n");
		warn("to message that has previously been replied Feature Abort to with [Unrecognized Opcode].\n");
	}
	else
		la_info[la].feature_aborted[opcode].ts = ts_now;

	cec_msg_reply_feature_abort(msg, reason);
	transmit(node, msg);
}

static bool exit_standby(struct node *node)
{
	/* Cancel any standby request that was pending. */
	node->state.record_received_standby = false;

	if (node->state.power_status == CEC_OP_POWER_STATUS_STANDBY ||
	    node->state.power_status == CEC_OP_POWER_STATUS_TO_STANDBY) {
		node->state.old_power_status = node->state.power_status;
		node->state.power_status = CEC_OP_POWER_STATUS_ON;
		node->state.power_status_changed_time = time(nullptr);
		dev_info("Changing state to on\n");
		return true;
	}
	return false;
}

bool enter_standby(struct node *node)
{
	if (node->state.power_status == CEC_OP_POWER_STATUS_ON ||
	    node->state.power_status == CEC_OP_POWER_STATUS_TO_ON) {
		/*
		 * Standby should not interrupt a recording in progress, but
		 * remember to go to standby once the recording is finished.
		 */
		if (node->state.one_touch_record_on) {
			node->state.record_received_standby = true;
			return false;
		}
		node->state.old_power_status = node->state.power_status;
		node->state.power_status = CEC_OP_POWER_STATUS_STANDBY;
		node->state.power_status_changed_time = time(nullptr);
		node->state.deck_skip_start = 0;
		node->state.record_received_standby = false;
		dev_info("Changing state to standby\n");
		return true;
	}
	return false;
}

static unsigned get_duration_ms(__u64 ts_a, __u64 ts_b)
{
	return (ts_a - ts_b) / 1000000;
}

static void rc_press_hold_stop(const struct state *state)
{
	unsigned mean_duration = state->rc_duration_sum / state->rc_press_hold_count;

	dev_info("Stop Press and Hold. Mean duration between User Control Pressed messages: %dms\n",
		 mean_duration);
	if (mean_duration < MIN_INITIATOR_REP_TIME) {
		warn("The mean duration between User Control Pressed messages is lower\n");
		warn("than the Minimum Initiator Repetition Time (200ms).\n");
	}
	if (mean_duration > MAX_INITIATOR_REP_TIME) {
		warn("The mean duration between User Control Pressed messages is higher\n");
		warn("than the Maximum Initiator Repetition Time (500ms).\n");
	}
}

static __u16 pa_common_mask(__u16 pa1, __u16 pa2)
{
	__u16 mask = 0xf000;

	for (int i = 0; i < 3; i++) {
		if ((pa1 & mask) != (pa2 & mask))
			break;
		mask = (mask >> 4) | 0xf000;
	}
	return mask << 4;
}
static bool pa_are_adjacent(__u16 pa1, __u16 pa2)
{
	const __u16 mask = pa_common_mask(pa1, pa2);
	const __u16 trail_mask = ((~mask) & 0xffff) >> 4;

	if (pa1 == CEC_PHYS_ADDR_INVALID || pa2 == CEC_PHYS_ADDR_INVALID || pa1 == pa2)
		return false;
	if ((pa1 & trail_mask) || (pa2 & trail_mask))
		return false;
	return !((pa1 & ~mask) && (pa2 & ~mask));
}

static bool pa_is_upstream_from(__u16 pa1, __u16 pa2)
{
	const __u16 mask = pa_common_mask(pa1, pa2);

	if (pa1 == CEC_PHYS_ADDR_INVALID || pa2 == CEC_PHYS_ADDR_INVALID)
		return false;
	return !(pa1 & ~mask) && (pa2 & ~mask);
}

static __u8 current_power_state(struct node *node)
{
	time_t t = time(nullptr);

	if (t - node->state.power_status_changed_time <= time_to_transient)
		return node->state.old_power_status;
	if (t - node->state.power_status_changed_time >= time_to_stable)
		return node->state.power_status;
	if (node->state.power_status == CEC_OP_POWER_STATUS_ON)
		return CEC_OP_POWER_STATUS_TO_ON;
	return CEC_OP_POWER_STATUS_TO_STANDBY;
}

static void aud_rate_msg_interval_check(struct node *node, __u64 ts_new)
{
	/*
	 * The interval since the last audio rate message is only relevant
	 * if the Source is currently in audio rate controlled mode
	 * (i.e. state.last_aud_rate_rx_ts != 0).
	 */
	__u64 ts_old = node->state.last_aud_rate_rx_ts;

	if (ts_old) {
		__u64 interval = ts_new - ts_old;

		if (interval > MAX_AUD_RATE_MSG_INTERVAL_NS) {
			warn("The interval since the last Audio Rate Control message was > 2s.\n");
			node->state.last_aud_rate_rx_ts = 0;
		}
	}
}

static void update_deck_state(struct node *node, unsigned me, __u8 deck_state_new)
{
	if (node->state.deck_state != deck_state_new) {
		node->state.deck_state = deck_state_new;

		if (node->state.deck_report_changes) {
			struct cec_msg msg;

			cec_msg_init(&msg, me, node->state.deck_report_changes_to);
			cec_msg_deck_status(&msg, node->state.deck_state);
			transmit(node, &msg);
		}
	}
}

static void processMsg(struct node *node, struct cec_msg &msg, unsigned me, __u8 type)
{
	__u8 to = cec_msg_destination(&msg);
	__u8 from = cec_msg_initiator(&msg);
	bool is_bcast = cec_msg_is_broadcast(&msg);
	__u16 remote_pa = (from < 15) ? node->remote_phys_addr[from] : CEC_PHYS_ADDR_INVALID;

	switch (msg.msg[1]) {

		/* OSD Display */

	case CEC_MSG_SET_OSD_STRING: {
		__u8 disp_ctl;
		char osd[14];

		if (to != 0 && to != 14)
			break;
		if (!node->has_osd_string)
			break;

		cec_ops_set_osd_string(&msg, &disp_ctl, osd);
		switch (disp_ctl) {
		case CEC_OP_DISP_CTL_DEFAULT:
		case CEC_OP_DISP_CTL_UNTIL_CLEARED:
		case CEC_OP_DISP_CTL_CLEAR:
			return;
		}
		cec_msg_reply_feature_abort(&msg, CEC_OP_ABORT_INVALID_OP);
		transmit(node, &msg);
		return;
	}


		/* Give Device Power Status */

	case CEC_MSG_GIVE_DEVICE_POWER_STATUS:
		cec_msg_set_reply_to(&msg, &msg);
		cec_msg_report_power_status(&msg, current_power_state(node));
		transmit(node, &msg);
		return;

	case CEC_MSG_REPORT_POWER_STATUS:
		// Nothing to do here for now.
		return;


		/* Standby */

	case CEC_MSG_STANDBY:
		enter_standby(node);
		return;


		/* One Touch Play and Routing Control */

	case CEC_MSG_ACTIVE_SOURCE: {
		__u16 phys_addr;

		cec_ops_active_source(&msg, &phys_addr);
		node->state.active_source_pa = phys_addr;
		dev_info("New active source: %x.%x.%x.%x\n", cec_phys_addr_exp(phys_addr));
		return;
	}
	case CEC_MSG_INACTIVE_SOURCE: {
		__u16 phys_addr;

		if (node->phys_addr)
			break;

		cec_ops_active_source(&msg, &phys_addr);
		if (node->state.active_source_pa != phys_addr)
			break;
		node->state.active_source_pa = 0;
		cec_msg_init(&msg, me, CEC_LOG_ADDR_BROADCAST);
		cec_msg_active_source(&msg, node->phys_addr);
		transmit(node, &msg);
		dev_info("New active source: 0.0.0.0\n");
		return;
	}
	case CEC_MSG_IMAGE_VIEW_ON:
	case CEC_MSG_TEXT_VIEW_ON:
		if (!cec_has_tv(1 << me))
			break;
		exit_standby(node);
		return;
	case CEC_MSG_SET_STREAM_PATH: {
		__u16 phys_addr;

		cec_ops_set_stream_path(&msg, &phys_addr);
		if (phys_addr != node->phys_addr)
			return;

		if (!cec_has_tv(1 << me))
			exit_standby(node);

		cec_msg_init(&msg, me, CEC_LOG_ADDR_BROADCAST);
		cec_msg_active_source(&msg, node->phys_addr);
		transmit(node, &msg);
		dev_info("Stream Path is directed to this device\n");
		return;
	}
	case CEC_MSG_ROUTING_INFORMATION: {
		__u8 la = cec_msg_initiator(&msg);

		if (cec_has_tv(1 << la) && la_info[la].phys_addr == 0)
			warn("TV (0) at 0.0.0.0 sent Routing Information.");
		return;
	}

		/* System Information */

	case CEC_MSG_GET_MENU_LANGUAGE:
		if (!cec_has_tv(1 << me))
			break;
		cec_msg_set_reply_to(&msg, &msg);
		cec_msg_set_menu_language(&msg, node->state.menu_language);
		transmit(node, &msg);
		return;
	case CEC_MSG_CEC_VERSION:
		return;
	case CEC_MSG_REPORT_PHYSICAL_ADDR: {
		__u16 phys_addr;
		__u8 prim_dev_type;

		cec_ops_report_physical_addr(&msg, &phys_addr, &prim_dev_type);
		if (from < 15) {
			node->remote_phys_addr[from] = phys_addr;
			node->remote_prim_devtype[from] = prim_dev_type;
		}
		return;
	}


		/* Remote Control Passthrough
		   (System Audio Control, Device Menu Control) */

	case CEC_MSG_USER_CONTROL_PRESSED: {
		struct cec_op_ui_command rc_press;
		unsigned new_state;
		unsigned duration;

		cec_ops_user_control_pressed(&msg, &rc_press);
		duration = get_duration_ms(msg.rx_ts, node->state.rc_press_rx_ts);

		new_state = PRESS;
		if (node->state.rc_state == NOPRESS)
			dev_info("Button press: %s\n", get_ui_cmd_string(rc_press.ui_cmd));
		else if (rc_press.ui_cmd != node->state.rc_ui_cmd) {
			/* We have not yet received User Control Released, but have received
			   another User Control Pressed with a different UI Command. */
			if (node->state.rc_state == PRESS_HOLD)
				rc_press_hold_stop(&node->state);
			dev_info("Button press (no User Control Released between): %s\n",
				 get_ui_cmd_string(rc_press.ui_cmd));

			/* The device shall send User Control Released if the time between
			   two messages is longer than the maximum Initiator Repetition Time. */
			if (duration > MAX_INITIATOR_REP_TIME)
				warn("Device waited more than the maximum Initiatior Repetition Time and should have sent a User Control Released message.");
		} else if (duration < FOLLOWER_SAFETY_TIMEOUT) {
			/* We have not yet received a User Control Released, but received
			   another User Control Pressed, with the same UI Command as the
			   previous, which means that the Press and Hold behavior should
			   be invoked. */
			new_state = PRESS_HOLD;
			if (node->state.rc_state != PRESS_HOLD) {
				dev_info("Start Press and Hold with button %s\n",
					 get_ui_cmd_string(rc_press.ui_cmd));
				node->state.rc_duration_sum = 0;
				node->state.rc_press_hold_count = 0;
			}
			node->state.rc_duration_sum += duration;
			node->state.rc_press_hold_count++;
		}

		node->state.rc_state = new_state;
		node->state.rc_ui_cmd = rc_press.ui_cmd;
		node->state.rc_press_rx_ts = msg.rx_ts;

		switch (rc_press.ui_cmd) {
		case CEC_OP_UI_CMD_VOLUME_UP:
			if (node->state.volume < VOLUME_MAX)
				node->state.volume++;
			break;
		case CEC_OP_UI_CMD_VOLUME_DOWN:
			if (node->state.volume > VOLUME_MIN)
				node->state.volume--;
			break;
		case CEC_OP_UI_CMD_MUTE:
			node->state.mute = !node->state.mute;
			break;
		case CEC_OP_UI_CMD_MUTE_FUNCTION:
			node->state.mute = true;
			break;
		case CEC_OP_UI_CMD_RESTORE_VOLUME_FUNCTION:
			node->state.mute = false;
			break;
		case CEC_OP_UI_CMD_POWER_TOGGLE_FUNCTION:
			if (!enter_standby(node))
				exit_standby(node);
			break;
		case CEC_OP_UI_CMD_POWER_OFF_FUNCTION:
			enter_standby(node);
			break;
		case CEC_OP_UI_CMD_POWER_ON_FUNCTION:
			exit_standby(node);
			break;
		}
		if (rc_press.ui_cmd >= CEC_OP_UI_CMD_VOLUME_UP && rc_press.ui_cmd <= CEC_OP_UI_CMD_MUTE) {
			dev_info("Volume: %d%s\n", node->state.volume, node->state.mute ? ", muted" : "");
			cec_msg_set_reply_to(&msg, &msg);
			cec_msg_report_audio_status(&msg, node->state.mute ? 1 : 0, node->state.volume);
			transmit(node, &msg);
		}

		return;
	}
	case CEC_MSG_USER_CONTROL_RELEASED:
		if (node->state.rc_state == PRESS_HOLD)
			rc_press_hold_stop(&node->state);

		if (node->state.rc_state == NOPRESS)
			dev_info("Button release (unexpected, %ums after last press)\n",
				 ts_to_ms(msg.rx_ts - node->state.rc_press_rx_ts));
		else if (get_duration_ms(msg.rx_ts, node->state.rc_press_rx_ts) > FOLLOWER_SAFETY_TIMEOUT)
			dev_info("Button release: %s (after timeout, %ums after last press)\n",
				 get_ui_cmd_string(node->state.rc_ui_cmd),
				 ts_to_ms(msg.rx_ts - node->state.rc_press_rx_ts));
		else
			dev_info("Button release: %s (%ums after last press)\n",
				 get_ui_cmd_string(node->state.rc_ui_cmd),
				 ts_to_ms(msg.rx_ts - node->state.rc_press_rx_ts));
		node->state.rc_state = NOPRESS;
		return;


		/* Device Menu Control */

	case CEC_MSG_MENU_REQUEST: {
		if (node->cec_version < CEC_OP_CEC_VERSION_2_0 &&
		    !cec_has_tv(1 << me)) {
			cec_msg_set_reply_to(&msg, &msg);
			cec_msg_menu_status(&msg, CEC_OP_MENU_STATE_ACTIVATED);
			transmit(node, &msg);
			return;
		}
		break;
	}
	case CEC_MSG_MENU_STATUS:
		if (node->cec_version < CEC_OP_CEC_VERSION_2_0 &&
		    cec_has_tv(1 << me))
			return;
		break;


		/* Deck Control */

	case CEC_MSG_GIVE_DECK_STATUS:
		if (!node->has_deck_ctl)
			break;

		__u8 status_req;
		cec_ops_give_deck_status(&msg, &status_req);

		switch (status_req) {
		case CEC_OP_STATUS_REQ_ON:
			node->state.deck_report_changes = true;
			node->state.deck_report_changes_to = cec_msg_initiator(&msg);
			fallthrough;
		case CEC_OP_STATUS_REQ_ONCE:
			cec_msg_set_reply_to(&msg, &msg);
			cec_msg_deck_status(&msg, node->state.deck_state);
			transmit(node, &msg);
			return;
		case CEC_OP_STATUS_REQ_OFF:
			node->state.deck_report_changes = false;
			return;
		default:
			reply_feature_abort(node, &msg, CEC_OP_ABORT_INVALID_OP);
			return;
		}
	case CEC_MSG_PLAY:
		if (!node->has_deck_ctl)
			break;

		__u8 deck_state;
		__u8 play_mode;

		cec_ops_play(&msg, &play_mode);

		switch (play_mode) {
		case CEC_OP_PLAY_MODE_PLAY_FWD:
			exit_standby(node);
			deck_state = CEC_OP_DECK_INFO_PLAY;
			break;
		case CEC_OP_PLAY_MODE_PLAY_REV:
			deck_state = CEC_OP_DECK_INFO_PLAY_REV;
			break;
		case CEC_OP_PLAY_MODE_PLAY_STILL:
			deck_state = CEC_OP_DECK_INFO_STILL;
			break;
		case CEC_OP_PLAY_MODE_PLAY_FAST_FWD_MIN:
		case CEC_OP_PLAY_MODE_PLAY_FAST_FWD_MED:
		case CEC_OP_PLAY_MODE_PLAY_FAST_FWD_MAX:
			deck_state = CEC_OP_DECK_INFO_FAST_FWD;
			break;
		case CEC_OP_PLAY_MODE_PLAY_FAST_REV_MIN:
		case CEC_OP_PLAY_MODE_PLAY_FAST_REV_MED:
		case CEC_OP_PLAY_MODE_PLAY_FAST_REV_MAX:
			deck_state = CEC_OP_DECK_INFO_FAST_REV;
			break;
		case CEC_OP_PLAY_MODE_PLAY_SLOW_FWD_MIN:
		case CEC_OP_PLAY_MODE_PLAY_SLOW_FWD_MED:
		case CEC_OP_PLAY_MODE_PLAY_SLOW_FWD_MAX:
			deck_state = CEC_OP_DECK_INFO_SLOW;
			break;
		case CEC_OP_PLAY_MODE_PLAY_SLOW_REV_MIN:
		case CEC_OP_PLAY_MODE_PLAY_SLOW_REV_MED:
		case CEC_OP_PLAY_MODE_PLAY_SLOW_REV_MAX:
			deck_state = CEC_OP_DECK_INFO_SLOW_REV;
			break;
		default:
			cec_msg_reply_feature_abort(&msg, CEC_OP_ABORT_INVALID_OP);
			transmit(node, &msg);
			return;
		}
		/* Only Play Forward and Play Still will close tray if open. */
		if (play_mode != CEC_OP_PLAY_MODE_PLAY_FWD &&
		    play_mode != CEC_OP_PLAY_MODE_PLAY_STILL &&
		    node->state.deck_state == CEC_OP_DECK_INFO_NO_MEDIA) {
			reply_feature_abort(node, &msg, CEC_OP_ABORT_INCORRECT_MODE);
			return;
		}
		node->state.deck_skip_start = 0;
		update_deck_state(node, me, deck_state);
		return;
	case CEC_MSG_DECK_CONTROL:
		if (!node->has_deck_ctl)
			break;

		__u8 deck_control_mode;

		cec_ops_deck_control(&msg, &deck_control_mode);

		switch (deck_control_mode) {
		case CEC_OP_DECK_CTL_MODE_STOP:
			deck_state = CEC_OP_DECK_INFO_STOP;
			node->state.deck_skip_start = 0;
			break;
		case CEC_OP_DECK_CTL_MODE_EJECT:
			exit_standby(node);
			deck_state = CEC_OP_DECK_INFO_NO_MEDIA;
			node->state.deck_skip_start = 0;
			break;
		case CEC_OP_DECK_CTL_MODE_SKIP_FWD:
			/* Skip Forward will not retract the deck tray. */
			if (node->state.deck_state == CEC_OP_DECK_INFO_NO_MEDIA) {
				reply_feature_abort(node, &msg, CEC_OP_ABORT_INCORRECT_MODE);
				return;
			}
			deck_state = CEC_OP_DECK_INFO_SKIP_FWD;
			node->state.deck_skip_start = msg.rx_ts;
			break;
		case CEC_OP_DECK_CTL_MODE_SKIP_REV:
			/* Skip Reverse will not retract the deck tray. */
			if (node->state.deck_state == CEC_OP_DECK_INFO_NO_MEDIA) {
				reply_feature_abort(node, &msg, CEC_OP_ABORT_INCORRECT_MODE);
				return;
			}
			deck_state = CEC_OP_DECK_INFO_SKIP_REV;
			node->state.deck_skip_start = msg.rx_ts;
			break;
		default:
			cec_msg_reply_feature_abort(&msg, CEC_OP_ABORT_INVALID_OP);
			transmit(node, &msg);
			return;
		}
		update_deck_state(node, me, deck_state);
		return;
	case CEC_MSG_DECK_STATUS:
		return;

	/* Tuner/Record/Timer Messages */

	case CEC_MSG_GIVE_TUNER_DEVICE_STATUS:
	case CEC_MSG_TUNER_DEVICE_STATUS:
	case CEC_MSG_SELECT_ANALOGUE_SERVICE:
	case CEC_MSG_SELECT_DIGITAL_SERVICE:
	case CEC_MSG_TUNER_STEP_DECREMENT:
	case CEC_MSG_TUNER_STEP_INCREMENT:
		process_tuner_msgs(node, msg, me, type);
		return;
	case CEC_MSG_RECORD_TV_SCREEN:
	case CEC_MSG_RECORD_ON:
	case CEC_MSG_RECORD_OFF:
	case CEC_MSG_RECORD_STATUS:
		process_record_msgs(node, msg, me, type);
		return;
	case CEC_MSG_SET_ANALOGUE_TIMER:
	case CEC_MSG_SET_DIGITAL_TIMER:
	case CEC_MSG_SET_EXT_TIMER:
	case CEC_MSG_CLEAR_ANALOGUE_TIMER:
	case CEC_MSG_CLEAR_DIGITAL_TIMER:
	case CEC_MSG_CLEAR_EXT_TIMER:
	case CEC_MSG_SET_TIMER_PROGRAM_TITLE:
	case CEC_MSG_TIMER_CLEARED_STATUS:
	case CEC_MSG_TIMER_STATUS:
		process_timer_msgs(node, msg, me, type);
		return;

		/* Dynamic Auto Lipsync */

	case CEC_MSG_REQUEST_CURRENT_LATENCY: {
		__u16 phys_addr;

		cec_ops_request_current_latency(&msg, &phys_addr);
		if (phys_addr == node->phys_addr) {
			cec_msg_init(&msg, me, from);
			cec_msg_report_current_latency(&msg, phys_addr,
						       node->state.video_latency,
						       node->state.low_latency_mode,
						       node->state.audio_out_compensated,
						       node->state.audio_out_delay);
			transmit(node, &msg);
		}
		return;
	}


		/* Audio Return Channel Control */

	case CEC_MSG_INITIATE_ARC:
		if (node->sink_has_arc_tx) {
			if (!pa_is_upstream_from(node->phys_addr, remote_pa) ||
			    !pa_are_adjacent(node->phys_addr, remote_pa)) {
				cec_msg_reply_feature_abort(&msg, CEC_OP_ABORT_REFUSED);
				transmit(node, &msg);
				return;
			}
			cec_msg_set_reply_to(&msg, &msg);
			cec_msg_report_arc_initiated(&msg);
			transmit(node, &msg);
			node->state.arc_active = true;
			dev_info("ARC is initiated");
			return;
		}
		break;
	case CEC_MSG_TERMINATE_ARC:
		if (node->sink_has_arc_tx) {
			if (!pa_is_upstream_from(node->phys_addr, remote_pa) ||
			    !pa_are_adjacent(node->phys_addr, remote_pa)) {
				cec_msg_reply_feature_abort(&msg, CEC_OP_ABORT_REFUSED);
				transmit(node, &msg);
				return;
			}
			cec_msg_set_reply_to(&msg, &msg);
			cec_msg_report_arc_terminated(&msg);
			transmit(node, &msg);
			node->state.arc_active = false;
			dev_info("ARC is terminated\n");
			return;
		}
		break;
	case CEC_MSG_REQUEST_ARC_INITIATION:
		if (node->source_has_arc_rx) {
			if (pa_is_upstream_from(node->phys_addr, remote_pa) ||
			    !pa_are_adjacent(node->phys_addr, remote_pa)) {
				cec_msg_reply_feature_abort(&msg, CEC_OP_ABORT_REFUSED);
				transmit(node, &msg);
				return;
			}
			cec_msg_set_reply_to(&msg, &msg);
			cec_msg_initiate_arc(&msg, false);
			transmit(node, &msg);
			dev_info("ARC initiation has been requested.");
			return;
		}
		break;
	case CEC_MSG_REQUEST_ARC_TERMINATION:
		if (node->source_has_arc_rx) {
			if (pa_is_upstream_from(node->phys_addr, remote_pa) ||
			    !pa_are_adjacent(node->phys_addr, remote_pa)) {
				cec_msg_reply_feature_abort(&msg, CEC_OP_ABORT_REFUSED);
				transmit(node, &msg);
				return;
			}
			cec_msg_set_reply_to(&msg, &msg);
			cec_msg_terminate_arc(&msg, false);
			transmit(node, &msg);
			dev_info("ARC initiation has been requested.");
			return;
		}
		break;
	case CEC_MSG_REPORT_ARC_INITIATED:
		node->state.arc_active = true;
		dev_info("ARC is initiated\n");
		return;
	case CEC_MSG_REPORT_ARC_TERMINATED:
		node->state.arc_active = false;
		dev_info("ARC is terminated\n");
		return;


		/* System Audio Control */

	case CEC_MSG_SYSTEM_AUDIO_MODE_REQUEST: {
		if (!cec_has_audiosystem(1 << me))
			break;

		__u16 phys_addr;

		cec_ops_system_audio_mode_request(&msg, &phys_addr);
		cec_msg_init(&msg, me, CEC_LOG_ADDR_BROADCAST);
		if (phys_addr != CEC_PHYS_ADDR_INVALID) {
			cec_msg_set_system_audio_mode(&msg, CEC_OP_SYS_AUD_STATUS_ON);
			transmit(node, &msg);
			node->state.sac_active = true;
		}
		else {
			cec_msg_set_system_audio_mode(&msg, CEC_OP_SYS_AUD_STATUS_OFF);
			transmit(node, &msg);
			node->state.sac_active = false;
		}
		dev_info("System Audio Mode: %s\n", node->state.sac_active ? "on" : "off");
		return;
	}
	case CEC_MSG_SET_SYSTEM_AUDIO_MODE: {
		__u8 system_audio_status;

		if (!cec_msg_is_broadcast(&msg)) {
			/* Directly addressed Set System Audio Mode is used to see
			   if the TV supports the feature. If we time out, we
			   signalize that we support SAC. */
			if (cec_has_tv(1 << me))
				return;

			break;
		}
		cec_ops_set_system_audio_mode(&msg, &system_audio_status);
		if (system_audio_status == CEC_OP_SYS_AUD_STATUS_ON)
			node->state.sac_active = true;
		else if (system_audio_status == CEC_OP_SYS_AUD_STATUS_OFF)
			node->state.sac_active = false;
		dev_info("System Audio Mode: %s\n", node->state.sac_active ? "on" : "off");
		return;
	}
	case CEC_MSG_REPORT_AUDIO_STATUS:
		return;
	case CEC_MSG_GIVE_SYSTEM_AUDIO_MODE_STATUS:
		if (!cec_has_audiosystem(1 << me))
			break;
		cec_msg_set_reply_to(&msg, &msg);
		cec_msg_system_audio_mode_status(&msg, node->state.sac_active ? CEC_OP_SYS_AUD_STATUS_ON :
						 CEC_OP_SYS_AUD_STATUS_OFF);
		transmit(node, &msg);
		fallthrough;
	case CEC_MSG_GIVE_AUDIO_STATUS:
		if (!cec_has_audiosystem(1 << me))
			break;
		cec_msg_set_reply_to(&msg, &msg);
		cec_msg_report_audio_status(&msg, node->state.mute ? 1 : 0, node->state.volume);
		transmit(node, &msg);
		return;
	case CEC_MSG_SYSTEM_AUDIO_MODE_STATUS:
		return;
	case CEC_MSG_REQUEST_SHORT_AUDIO_DESCRIPTOR: {
		if (!cec_has_audiosystem(1 << me))
			break;

		/* The list of formats that the follower 'supports' */
		const struct short_audio_desc supported_formats[] = {
			{ 2, SAD_FMT_CODE_AC3,
			  SAD_SAMPLE_FREQ_MASK_32 | SAD_SAMPLE_FREQ_MASK_44_1,
			  { 64 }, 0, 0 },
			{ 4, SAD_FMT_CODE_AC3,
			  SAD_SAMPLE_FREQ_MASK_32,
			  { 32 }, 0, 0 },
			{ 4, SAD_FMT_CODE_ONE_BIT_AUDIO,
			  SAD_SAMPLE_FREQ_MASK_48 | SAD_SAMPLE_FREQ_MASK_192,
			  { 123 }, 0, 0 },
			{ 8, SAD_FMT_CODE_EXTENDED,
			  SAD_SAMPLE_FREQ_MASK_96,
			  { 0 }, 0, SAD_EXT_TYPE_DRA },
			{ 2, SAD_FMT_CODE_EXTENDED,
			  SAD_SAMPLE_FREQ_MASK_176_4,
			  { SAD_FRAME_LENGTH_MASK_960 | SAD_FRAME_LENGTH_MASK_1024 }, 1, SAD_EXT_TYPE_MPEG4_HE_AAC_SURROUND },
			{ 2, SAD_FMT_CODE_EXTENDED,
			  SAD_SAMPLE_FREQ_MASK_44_1,
			  { SAD_BIT_DEPTH_MASK_16 | SAD_BIT_DEPTH_MASK_24 }, 0, SAD_EXT_TYPE_LPCM_3D_AUDIO },
		};

		__u8 num_descriptors, audio_format_id[4], audio_format_code[4];
		__u32 descriptors[4];
		std::string format_list;

		cec_ops_request_short_audio_descriptor(&msg, &num_descriptors, audio_format_id, audio_format_code);
		if (num_descriptors == 0) {
			warn("Got Request Short Audio Descriptor with no operands");
			reply_feature_abort(node, &msg, CEC_OP_ABORT_INVALID_OP);
			return;
		}

		unsigned found_descs = 0;

		for (int i = 0; i < num_descriptors; i++)
			format_list += audio_format_id_code2s(audio_format_id[i], audio_format_code[i]) + ",";
		format_list.erase(format_list.end() - 1);
		dev_info("Requested descriptors: %s\n", format_list.c_str());
		for (unsigned i = 0; i < num_descriptors; i++) {
			for (const auto &supported_format : supported_formats) {
				if (found_descs >= 4)
					break;
				if ((audio_format_id[i] == 0 &&
				     audio_format_code[i] == supported_format.format_code) ||
				    (audio_format_id[i] == 1 &&
				     audio_format_code[i] == supported_format.extension_type_code))
					sad_encode(&supported_format, &descriptors[found_descs++]);
			}
		}

		if (found_descs > 0) {
			cec_msg_set_reply_to(&msg, &msg);
			cec_msg_report_short_audio_descriptor(&msg, found_descs, descriptors);
			transmit(node, &msg);
		}
		else
			reply_feature_abort(node, &msg, CEC_OP_ABORT_INVALID_OP);
		return;
	}


		/* Device OSD Transfer */

	case CEC_MSG_SET_OSD_NAME:
		return;


		/* Audio Rate Control */

	case CEC_MSG_SET_AUDIO_RATE:
		if (!node->has_aud_rate)
			break;

		switch (msg.msg[2]) {
		case CEC_OP_AUD_RATE_OFF:
			aud_rate_msg_interval_check(node, msg.rx_ts);
			node->state.last_aud_rate_rx_ts = 0;
			return;
		case CEC_OP_AUD_RATE_WIDE_STD:
		case CEC_OP_AUD_RATE_WIDE_FAST:
		case CEC_OP_AUD_RATE_WIDE_SLOW:
		case CEC_OP_AUD_RATE_NARROW_STD:
		case CEC_OP_AUD_RATE_NARROW_FAST:
		case CEC_OP_AUD_RATE_NARROW_SLOW:
			aud_rate_msg_interval_check(node, msg.rx_ts);
			node->state.last_aud_rate_rx_ts = msg.rx_ts;
			return;
		default:
			cec_msg_reply_feature_abort(&msg, CEC_OP_ABORT_INVALID_OP);
			transmit(node, &msg);
			break;
		}
		break;


		/* CDC */

	case CEC_MSG_CDC_MESSAGE: {
		switch (msg.msg[4]) {
		case CEC_MSG_CDC_HEC_DISCOVER:
			__u16 phys_addr;

			cec_msg_init(&msg, me, CEC_LOG_ADDR_BROADCAST);
			cec_ops_cdc_hec_discover(&msg, &phys_addr);
			cec_msg_cdc_hec_report_state(&msg, phys_addr,
						     CEC_OP_HEC_FUNC_STATE_NOT_SUPPORTED,
						     CEC_OP_HOST_FUNC_STATE_NOT_SUPPORTED,
						     CEC_OP_ENC_FUNC_STATE_EXT_CON_NOT_SUPPORTED,
						     CEC_OP_CDC_ERROR_CODE_NONE,
						     1, 0); // We do not support HEC on any HDMI connections
			transmit(node, &msg);
			return;
		}
	}


		/* Core */

	case CEC_MSG_FEATURE_ABORT:
		return;
	default:
		break;
	}

	if (is_bcast)
		return;

	reply_feature_abort(node, &msg);
}

static void poll_remote_devs(struct node *node, unsigned me)
{
	node->remote_la_mask = 0;
	for (unsigned short & i : node->remote_phys_addr)
		i = CEC_PHYS_ADDR_INVALID;

	if (!(node->caps & CEC_CAP_TRANSMIT))
		return;

	for (unsigned i = 0; i < 15; i++) {
		struct cec_msg msg;

		cec_msg_init(&msg, me, i);

		doioctl(node, CEC_TRANSMIT, &msg);
		if (msg.tx_status & CEC_TX_STATUS_OK) {
			node->remote_la_mask |= 1 << i;
			cec_msg_init(&msg, me, i);
			cec_msg_give_physical_addr(&msg, true);
			doioctl(node, CEC_TRANSMIT, &msg);
			if (cec_msg_status_is_ok(&msg))
				node->remote_phys_addr[i] = (msg.msg[2] << 8) | msg.msg[3];
		}
	}
}

static void update_programmed_timers(struct node *node)
{
	std::set<struct Timer>::iterator it = programmed_timers.begin();
	/* Use the current minute because timers do not have second precision. */
	time_t current_minute = time(nullptr) / 60;
	time_t timer_start_minute = it->start_time / 60;
	time_t timer_end_minute = (it->start_time + it->duration) / 60;

	/* Start the timed recording only if the deck is not already recording. */
	if (timer_start_minute == current_minute && !node->state.one_touch_record_on) {
		node->state.one_touch_record_on = true;
		node->state.recording_controlled_by_timer = true;
		print_timers(node);
	}

	/* Delete an overlapped timer. Recording will be at best incomplete. */
	if (timer_start_minute < current_minute &&
	    (!node->state.recording_controlled_by_timer || !node->state.one_touch_record_on)) {
		programmed_timers.erase(*it);
		if (show_info)
			printf("Deleted overlapped timer.\n");
		print_timers(node);
	}

	if (timer_end_minute != current_minute || !node->state.recording_controlled_by_timer)
		return;

	/* Delete finished timers. */
	node->state.one_touch_record_on = false;
	node->state.recording_controlled_by_timer = false;
	node->state.media_space_available -= it->duration; /* 1 MB per second */
	/*
	 * TODO: We are only ever decreasing the amount of space available,
	 * there is no heuristic that reclaims the space.
	 */

	if (it->recording_seq) {
		struct tm *last_start_time = localtime(&(it->start_time));
		int next_wday = (last_start_time->tm_wday + 1) % 7;
		int days_to_move_ahead = 1;

		while ((it->recording_seq & (1 << next_wday)) == 0) {
			days_to_move_ahead++;
			next_wday = (next_wday + 1) % 7;
		}
		struct Timer next_timer = {};
		next_timer = *it;
		last_start_time->tm_mday += days_to_move_ahead;
		last_start_time->tm_isdst = -1;
		next_timer.start_time = mktime(last_start_time);
		programmed_timers.insert(next_timer);
	}
	programmed_timers.erase(*it);
	if (show_info)
		printf("Deleted finished timer.\n");
	print_timers(node);
	/*
	 * If the finished timer was recording, and standby was received during recording,
	 * enter standby when the recording stops unless recording device is the active source.
	 */
	if (node->state.record_received_standby) {
		if (node->phys_addr != node->state.active_source_pa)
			enter_standby(node);
		node->state.record_received_standby = false;
	}
}

void testProcessing(struct node *node, bool wallclock)
{
	struct cec_log_addrs laddrs;
	fd_set rd_fds;
	fd_set ex_fds;
	int fd = node->fd;
	__u32 mode = CEC_MODE_INITIATOR | CEC_MODE_FOLLOWER;
	unsigned me;
	unsigned last_poll_la = 15;
	__u8 last_pwr_state = current_power_state(node);
	time_t last_pwr_status_toggle = time(nullptr);

	clock_gettime(CLOCK_MONOTONIC, &start_monotonic);
	gettimeofday(&start_timeofday, nullptr);

	doioctl(node, CEC_S_MODE, &mode);
	doioctl(node, CEC_ADAP_G_LOG_ADDRS, &laddrs);
	me = laddrs.log_addr[0];
	__u8 type = laddrs.log_addr_type[0];

	poll_remote_devs(node, me);

	while (true) {
		int res;
		struct timeval timeval = {};

		fflush(stdout);
		timeval.tv_sec = 1;
		FD_ZERO(&rd_fds);
		FD_ZERO(&ex_fds);
		FD_SET(fd, &rd_fds);
		FD_SET(fd, &ex_fds);
		res = select(fd + 1, &rd_fds, nullptr, &ex_fds, &timeval);
		if (res < 0)
			break;
		if (FD_ISSET(fd, &ex_fds)) {
			struct cec_event ev;

			res = doioctl(node, CEC_DQEVENT, &ev);
			if (res == ENODEV) {
				printf("Device was disconnected.\n");
				break;
			}
			if (res)
				continue;
			log_event(ev, wallclock);
			if (ev.event == CEC_EVENT_STATE_CHANGE) {
				dev_info("CEC adapter state change.\n");
				node->phys_addr = ev.state_change.phys_addr;
				node->adap_la_mask = ev.state_change.log_addr_mask;
				if (node->adap_la_mask) {
					doioctl(node, CEC_ADAP_G_LOG_ADDRS, &laddrs);
					me = laddrs.log_addr[0];
				} else {
					node->state.active_source_pa = CEC_PHYS_ADDR_INVALID;
					me = CEC_LOG_ADDR_INVALID;
				}
				memset(la_info, 0, sizeof(la_info));
			}
		}
		if (FD_ISSET(fd, &rd_fds)) {
			struct cec_msg msg = { };

			res = doioctl(node, CEC_RECEIVE, &msg);
			if (res == ENODEV) {
				printf("Device was disconnected.\n");
				break;
			}
			if (res)
				continue;

			__u8 from = cec_msg_initiator(&msg);
			__u8 to = cec_msg_destination(&msg);
			__u8 opcode = cec_msg_opcode(&msg);

			if (node->ignore_la[from])
				continue;
			if (node->ignore_opcode[msg.msg[1]] & (1 << from))
				continue;

			if (from != CEC_LOG_ADDR_UNREGISTERED &&
			    la_info[from].feature_aborted[opcode].ts &&
			    ts_to_ms(get_ts() - la_info[from].feature_aborted[opcode].ts) < 200) {
				warn("Received message %s from LA %d (%s) less than 200 ms after\n",
				     opcode2s(&msg).c_str(), from, cec_la2s(from));
				warn("replying Feature Abort (not [Unrecognized Opcode]) to the same message.\n");
			}
			if (from != CEC_LOG_ADDR_UNREGISTERED && !la_info[from].ts)
				dev_info("Logical address %d (%s) discovered.\n", from, cec_la2s(from));
			if (show_msgs) {
				printf("    %s to %s (%d to %d): ",
				       cec_la2s(from), to == 0xf ? "all" : cec_la2s(to), from, to);
				cec_log_msg(&msg);
				if (show_info)
					printf("\tSequence: %u Rx Timestamp: %s\n",
					       msg.sequence, ts2s(msg.rx_ts, wallclock).c_str());
			}
			if (node->adap_la_mask)
				processMsg(node, msg, me, type);
		}

		__u8 pwr_state = current_power_state(node);
		if (node->cec_version >= CEC_OP_CEC_VERSION_2_0 &&
		    last_pwr_state != pwr_state &&
		    (time_to_stable > 2 || pwr_state < CEC_OP_POWER_STATUS_TO_ON)) {
			struct cec_msg msg;

			cec_msg_init(&msg, me, CEC_LOG_ADDR_BROADCAST);
			cec_msg_report_power_status(&msg, pwr_state);
			transmit(node, &msg);
			last_pwr_state = pwr_state;
		}

		if (node->state.toggle_power_status && cec_has_tv(1 << me) &&
		    (time(nullptr) - last_pwr_status_toggle > node->state.toggle_power_status)) {
			last_pwr_status_toggle = time(nullptr);
			if (pwr_state & 1) // standby or to-standby
				exit_standby(node);
			else
				enter_standby(node);
		}

		__u64 ts_now = get_ts();
		unsigned poll_la = ts_to_s(ts_now) % 16;

		if (poll_la != me &&
		    poll_la != last_poll_la && poll_la < 15 && la_info[poll_la].ts &&
		    ts_to_ms(ts_now - la_info[poll_la].ts) > POLL_PERIOD) {
			struct cec_msg msg;

			cec_msg_init(&msg, me, poll_la);
			transmit(node, &msg);
			if (msg.tx_status & CEC_TX_STATUS_NACK) {
				dev_info("Logical address %d stopped responding to polling message.\n", poll_la);
				memset(&la_info[poll_la], 0, sizeof(la_info[poll_la]));
				node->remote_la_mask &= ~(1 << poll_la);
				node->remote_phys_addr[poll_la] = CEC_PHYS_ADDR_INVALID;
			}
		}
		last_poll_la = poll_la;

		unsigned ms_since_press = ts_to_ms(ts_now - node->state.rc_press_rx_ts);

		if (ms_since_press > FOLLOWER_SAFETY_TIMEOUT) {
			if (node->state.rc_state == PRESS_HOLD)
				rc_press_hold_stop(&node->state);
			else if (node->state.rc_state == PRESS) {
				dev_info("Button timeout: %s\n", get_ui_cmd_string(node->state.rc_ui_cmd));
				node->state.rc_state = NOPRESS;
			}
		}

		if (node->has_aud_rate)
			aud_rate_msg_interval_check(node, ts_now);

		if (node->state.deck_skip_start && ts_now - node->state.deck_skip_start > MAX_DECK_SKIP_NS) {
			node->state.deck_skip_start = 0;
			update_deck_state(node, me, CEC_OP_DECK_INFO_PLAY);
		}

		if (!programmed_timers.empty())
			update_programmed_timers(node);
	}
	mode = CEC_MODE_INITIATOR;
	doioctl(node, CEC_S_MODE, &mode);
}
