/*
 * ddc-ci.c: DDC-CI utilities, Linux-only
 *
 * Copyright (c) 2015 - 2017, Přemysl Eric Janouch 
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 */
// Sources: ddcciv1r1.pdf, i2c-dev.c in Linux, ddccontrol source code,
//   http://www.boichat.ch/nicolas/ddcci/specs.html was also helpful
#include 
#include 
#ifndef I2C_FUNC_I2C
// Fuck you, openSUSE, for fucking up the previous file, see e.g.
// https://github.com/solettaproject/soletta/commit/427f47f
#include 
#endif
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
log_message_custom (void *user_data, const char *quote, const char *fmt,
	va_list ap)
{
	(void) user_data;
	FILE *stream = stdout;
	fprintf (stream, PROGRAM_NAME ": ");
	fputs (quote, stream);
	vfprintf (stream, fmt, ap);
	fputs ("\n", stream);
}
static void
wait_ms (long ms)
{
	struct timespec ts = { ms / 1000, (ms % 1000) * 1000 * 1000 };
	nanosleep (&ts, NULL);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#define DDC_LENGTH_XOR 0x80
enum
{
	DDC_ADDRESS_HOST          = 0x50,   ///< Bus master's base address
	DDC_ADDRESS_DISPLAY       = 0x6E    ///< The display's base address
};
enum { I2C_WRITE, I2C_READ };
enum
{
	DDC_GET_VCP_FEATURE       = 0x01,   ///< Request info about a feature
	DDC_GET_VCP_FEATURE_REPLY = 0x02,   ///< Feature info response
	DDC_SET_VCP_FEATURE       = 0x03    ///< Set or activate a feature
};
enum
{
	VCP_BRIGHTNESS            = 0x10,   ///< Standard VCP opcode for brightness
	VCP_CONTRAST              = 0x12,   ///< Standard VCP opcode for contrast
	VCP_INPUT_SOURCE          = 0x60    ///< Standard VCP opcode for input
};
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static bool
check_edid (int fd, struct error **e)
{
	uint8_t edid_req[] = { 0x00 };
	uint8_t buf[8] = "";
	struct i2c_msg bufs[] =
	{
		{ .addr = 0x50, .flags = 0,
		  .len = 1, .buf = edid_req },
		{ .addr = 0x50, .flags = I2C_M_RD,
		  .len = sizeof buf, .buf = buf },
	};
	struct i2c_rdwr_ioctl_data data;
	data.msgs = bufs;
	data.nmsgs = 2;
	if (ioctl (fd, I2C_RDWR, &data) < 0)
		return error_set (e, "%s: %s", "ioctl", strerror (errno));
	if (memcmp ("\x00\xFF\xFF\xFF\xFF\xFF\xFF\x00", buf, sizeof buf))
		return error_set (e, "invalid EDID");
	return true;
}
static bool
is_a_display (int fd, struct error **e)
{
	struct stat st;
	if (fstat (fd, &st) < 0)
		return error_set (e, "%s: %s", "fstat", strerror (errno));
	unsigned long funcs;
	if (!(st.st_mode & S_IFCHR)
	 || ioctl (fd, I2C_FUNCS, &funcs) < 0
	 || !(funcs & I2C_FUNC_I2C))
		return error_set (e, "not an I2C device");
	return check_edid (fd, e);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static bool
ddc_send (int fd, unsigned command, void *args, size_t args_len,
	struct error **e)
{
	struct str buf = str_make ();
	str_pack_u8 (&buf, DDC_ADDRESS_HOST | I2C_READ);
	str_pack_u8 (&buf, DDC_LENGTH_XOR | (args_len + 1));
	str_pack_u8 (&buf, command);
	str_append_data (&buf, args, args_len);
	unsigned xor = DDC_ADDRESS_DISPLAY;
	for (size_t i = 0; i < buf.len; i++)
		xor ^= buf.str[i];
	str_pack_u8 (&buf, xor);
	struct i2c_msg msg =
	{
		// The driver unshifts it back
		.addr = DDC_ADDRESS_DISPLAY >> 1, .flags = 0,
		.len = buf.len, .buf = (uint8_t *) buf.str,
	};
	struct i2c_rdwr_ioctl_data data;
	data.msgs = &msg;
	data.nmsgs = 1;
	bool failed = ioctl (fd, I2C_RDWR, &data) < 0;
	str_free (&buf);
	if (failed)
		return error_set (e, "%s: %s", "ioctl", strerror (errno));
	return true;
}
static bool
ddc_read (int fd, unsigned *command, void *out_buf, size_t *n_read,
	struct error **e)
{
	uint8_t buf[128] = "";
	struct i2c_msg msg =
	{
		// The driver unshifts it back
		.addr = DDC_ADDRESS_DISPLAY >> 1, .flags = I2C_M_RD,
		.len = sizeof buf, .buf = buf,
	};
	struct i2c_rdwr_ioctl_data data;
	data.msgs = &msg;
	data.nmsgs = 1;
	if (ioctl (fd, I2C_RDWR, &data) < 0)
		return error_set (e, "%s: %s", "ioctl", strerror (errno));
	struct msg_unpacker unpacker = msg_unpacker_make (buf, sizeof buf);
	uint8_t sender, length, cmd;
	(void) msg_unpacker_u8 (&unpacker, &sender);
	(void) msg_unpacker_u8 (&unpacker, &length);
	(void) msg_unpacker_u8 (&unpacker, &cmd);
	if (sender != (DDC_ADDRESS_DISPLAY | I2C_WRITE) || !(length & 0x80))
		return error_set (e, "invalid response");
	if (!(length ^= 0x80))
		return error_set (e, "NULL response");
	// TODO: also check the checksum
	*command = cmd;
	memcpy (out_buf, unpacker.data + unpacker.offset, (*n_read = length - 1));
	return true;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct vcp_feature_readout
{
	uint8_t type;                       ///< Type of the value
	int16_t max;                        ///< Maximum value
	int16_t cur;                        ///< Current value
};
static bool
vcp_get_feature (int fd, uint8_t feature, struct vcp_feature_readout *out,
	struct error **e)
{
	uint8_t get_req[] = { feature };
	if (!ddc_send (fd, DDC_GET_VCP_FEATURE, get_req, sizeof get_req, e))
		return false;
	wait_ms (40);
	unsigned command = 0;
	uint8_t buf[128] = "";
	size_t len = 0;
	if (!ddc_read (fd, &command, buf, &len, e))
		return false;
	if (command != DDC_GET_VCP_FEATURE_REPLY || len != 7)
		return error_set (e, "invalid response");
	struct msg_unpacker unpacker = msg_unpacker_make (buf, len);
	uint8_t result;     msg_unpacker_u8  (&unpacker, &result);
	uint8_t vcp_opcode; msg_unpacker_u8  (&unpacker, &vcp_opcode);
	msg_unpacker_u8  (&unpacker, &out->type);
	msg_unpacker_i16 (&unpacker, &out->max);
	msg_unpacker_i16 (&unpacker, &out->cur);
	if (result == 0x01)
		return error_set (e, "error reported by monitor");
	if (result != 0x00
	 || vcp_opcode != feature)
		return error_set (e, "invalid response");
	// These are unsigned but usually just one byte long
	if (out->max < 0 || out->cur < 0)
		return error_set (e, "capability range overflow");
	return true;
}