/* Copyright (C) 2011 Andy Getzendanner                                       //
// Hereby licensed AS-IS to anyone who wants it                               //
// Responsibility and liability for unintended consequences including         //
// hardware damage are hereby disclaimed; USE ONLY AT YOUR OWN RISK           //
// Note: compile with at least -O due to inlined SMBus functions              //
// Note: errors in i2c-dev.h likely result from using the kernel-internal     //
//       version of that file rather than the userspace-facing one from the   //
//       i2c-tools package, available from:                                   //
//           http://www.lm-sensors.org/wiki/I2CTools                          */

#include <stdlib.h>
#include <stdio.h>
#include <inttypes.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <linux/i2c-dev.h>

#define EDID_I2C_ADDR		0x50
#define EDID_BLKSIZE		128

#define iferror(COND, FUNC)	_iferror(COND, FUNC, __LINE__)
#define ifwarn(COND, FUNC)	_ifwarn(COND, FUNC, __LINE__)
#define sizeel(X) sizeof(*X)
#define numel(X) (sizeof(X)/sizeel(X))

const uint8_t EDID_HEADER[8] = {0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00};

int main(int argc, char *argv[], char *envp[]);
void usage(void) __attribute__ ((__noreturn__));
void _iferror(int cond, const char *func, int line);
void _ifwarn(int cond, const char *func, int line);
void fhexbuf(FILE *stream, const uint8_t *buf, size_t size);
uint8_t checksum(const uint8_t *buf, size_t size);
void checkedid(const uint8_t *edid, int fatal);

int main(int argc, char *argv[], char *envp[]) {
	int quiet = 0;
	int force = 0;
	argv++; argc--;
	
	if (argc < 1) usage();
	if (!strcmp(argv[0], "-q")) {
		quiet = 1;
		argv++; argc--;
		if (argc < 1) usage();
	}
	if (!strcmp(argv[0], "-f")) {
		force = 1;
		argv++; argc--;
		if (argc < 1) usage();
	}
	int fd = open(argv[0], O_RDWR);
	iferror(fd < 0, "open");
	argv++; argc--;
	
	int ret = ioctl(fd, I2C_SLAVE, (ptrdiff_t) EDID_I2C_ADDR);
	iferror(ret == -1, "ioctl");
	
	if (argc != 1) usage();
	if (!strcmp(argv[0], "read")) {
		uint8_t buf[EDID_BLKSIZE];
		size_t addr = 0x00;
		for (size_t addr = 0; addr < sizeof(buf); addr++) {
			int32_t rb = i2c_smbus_read_byte_data(fd, (uint8_t) addr);
			iferror(rb == -1, "i2c_smbus_read_byte_data");
			buf[addr] = (uint8_t) rb;
		}
		if (!quiet) fhexbuf(stderr, buf, sizeof(buf));
		checkedid(buf, 0);
		size_t wbc = fwrite(buf, sizeel(buf), numel(buf), stdout);
		iferror(ferror(stdout), "fwrite");
		iferror(wbc < numel(buf), "fwrite");
		return EXIT_SUCCESS;
	} else if (!strcmp(argv[0], "write")) {
		uint8_t buf[EDID_BLKSIZE];
		size_t rbc = fread(buf, sizeel(buf), numel(buf), stdin);
		iferror(ferror(stdin), "fread");
		iferror(rbc < numel(buf), "fread");
		if (!quiet) fhexbuf(stderr, buf, sizeof(buf));
		checkedid(buf, !force);
		for (size_t addr = 0; addr < sizeof(buf); addr++) {
			int32_t smret = i2c_smbus_write_byte_data(fd, (uint8_t) addr,
			                                          buf[addr]);
			iferror(smret == -1, "i2c_smbus_write_byte_data"); 
		}
		return EXIT_SUCCESS;
	} else if (!strcmp(argv[0], "check")) {
		uint8_t buf[EDID_BLKSIZE];
		size_t rbc = fread(buf, sizeel(buf), numel(buf), stdin);
		iferror(ferror(stdin), "fread");
		iferror(rbc < numel(buf), "fread");
		if (!quiet) fhexbuf(stderr, buf, sizeof(buf));
		checkedid(buf, 1);
		return EXIT_SUCCESS;
	} else if (!strcmp(argv[0], "fix")) {
		uint8_t buf[EDID_BLKSIZE];
		size_t rbc = fread(buf, sizeel(buf), numel(buf), stdin);
		iferror(ferror(stdin), "fread");
		iferror(rbc < numel(buf), "fread");
		if (!quiet) fhexbuf(stderr, buf, sizeof(buf));
		int change = 0;
		if (memcmp(buf, EDID_HEADER, sizeof(EDID_HEADER))) {
			(void) fprintf(stderr, "INFO  at %d: Fixing header\n", __LINE__);
			(void) memcpy(buf, EDID_HEADER, sizeof(EDID_HEADER));
			change = 1;
		}
		uint8_t cksum = 0x00 - checksum(buf, EDID_BLKSIZE - 1);
		if (buf[EDID_BLKSIZE - 1] != cksum) {
			(void) fprintf(stderr, "INFO  at %d: Fixing checksum\n", __LINE__);
			buf[EDID_BLKSIZE - 1] = cksum;
			change = 1;
		}
		if (change && !quiet) fhexbuf(stderr, buf, sizeof(buf));
		size_t wbc = fwrite(buf, sizeel(buf), numel(buf), stdout);
		iferror(ferror(stdout), "fwrite");
		iferror(wbc < numel(buf), "fwrite");
		return EXIT_SUCCESS;
	} else usage();
}

void usage(void) {
	(void) fputs("Usage: edid-tool [-q] [-f] i2cdev read|write|tryfix\n"
	             "       i2cdev - probably like /dev/i2c-%d\n"
	             "       read   - outputs a binary blob to stdout\n"
	             "       write  - reads a binary blob from stdin\n"
	             "       check  - reads a binary blob from stdin and checks its header and\n"
	             "                checksum\n"
	             "       fix    - reads a binary blob from stdin, corrects the header and\n"
	             "                checksum if necessary, and writes to stdout\n"
	             "       -q     - suppresses output of hex block to stderr\n"
	             "       -f     - ignores checksum and header errors when writing EDID checksum\n"
	             "       argument order must be as specified above\n",
	             stderr);
	exit(EXIT_FAILURE);
}

void _iferror(int cond, const char *func, int line) {
	if (cond) {
		(void) fprintf(stderr, "ERROR at %d: %s() failed: %s\n", line, func,
		               strerror(errno));
		exit(EXIT_FAILURE);
	} else return;
}

void _ifwarn(int cond, const char *func, int line) {
	if (cond) (void) fprintf(stderr, "WARN  at %d: %s() failed: %s\n", line,
	                         func, strerror(errno));
	return;
}

void fhexbuf(FILE *stream, const uint8_t *buf, size_t size) {
	(void) fprintf(stream, "          ");
	for (int addr = 0; addr < 0x10; addr++)
		(void) fprintf(stream, (addr % 0x04 == 0x03) ? "%2x  " : "%2x ",
		               addr);
	(void) fputc(' ', stream);
	for (int addr = 0; addr < 0x10; addr++) {
		(void) fprintf(stream, "%1x", addr);
		if ((addr % 0x04 == 0x03) && (addr % 0x10 != 0x0f))
			(void) fputc(' ', stream);
	}
	(void) fputc(' ', stream);
	(void) fputc('\n', stream);
	for (size_t rowaddr = 0; rowaddr < size; rowaddr += 0x10) {
		(void) fprintf(stream, "%08zx  ", rowaddr);
		for (size_t addr = rowaddr;
		     (addr < rowaddr + 0x10) && (addr < size); addr++)
			(void) fprintf(stream,
			               (addr % 0x04 == 0x03) ? "%02"PRIx8"  " :
			                                       "%02"PRIx8" ",
			               buf[addr]);
		for (size_t addr = size; addr < rowaddr + 0x10; addr++)
			(void) fprintf(stream, (addr % 0x04 == 0x03) ? "    " : "   ",
			                       buf[addr]);
		(void) fputc('|', stream);
		for (size_t addr = rowaddr;
		     (addr < rowaddr + 0x10) && (addr < size); addr++) {
			(void) fputc(isprint(buf[addr]) ? buf[addr] : '.', stream);
			if ((addr % 0x04 == 0x03) &&
			    (addr % 0x10 != 0x0f) &&
			    (addr < size - 1))
				(void) fputc(' ', stream);
		}
		(void) fputc('|', stream);
		(void) fputc('\n', stream);
	}
	return;
}

inline uint8_t checksum(const uint8_t *buf, size_t size) {
	uint8_t sum = 0x00;
	while (size--) sum += buf[size];
	return sum;
}

void checkedid(const uint8_t *edid, int fatal) {
	int bad = 0;
	if (memcmp(edid, EDID_HEADER, sizeof(EDID_HEADER))) {
		(void) fprintf(stderr, "%s at %d: Bad header: 0x"
			                   "%02"PRIx8"%02"PRIx8" %02"PRIx8"%02"PRIx8" "
			                   "%02"PRIx8"%02"PRIx8" %02"PRIx8"%02"PRIx8"\n",
			           fatal ? "ERROR" : "WARN", __LINE__,
			           edid[0], edid[1], edid[2], edid[3],
			           edid[4], edid[5], edid[6], edid[7]);
		bad = 1;
	}
	uint8_t cksum = checksum(edid, EDID_BLKSIZE);
	if (cksum) {
		(void) fprintf(stderr, "%s at %d: Bad checksum: 0x%02"PRIx8"\n",
		               fatal ? "ERROR" : "WARN", __LINE__, cksum);
		bad = 1;
	}
	if (fatal && bad) exit(EXIT_FAILURE);
	else return;
}