/* SPDX-License-Identifier: GPL-3.0-or-later */
/* SPDX-FileCopyrightText: 2024 Riku Viitanen <riku.viitanen@protonmail.com> */

/* Dumps information about MXM support on System BIOS.  Based on the
 * MXM 3.0 software spec, may or may not work on older versions. */

#include <stddef.h>

#include <stdint.h>
#include <stdio.h>
#include <string.h>


uint8_t hexdump(const unsigned char *data, size_t length) {
	int wrap_counter = 16;
	uint8_t sum = 0;
	size_t i = 0;
	while (i<length) {
		sum += data[i];
		printf("%02x", data[i++]);
		if (--wrap_counter) {
			putchar(' ');
			if (wrap_counter == 8)
				putchar(' ');
		} else {
			putchar('\n');
			wrap_counter = 16;
		}
	}
	putchar('\n');

	return sum;
}

/* Also used for validating a checksum.
 * Returns zero if the data (including checksum) is valid. */
uint8_t checksum(const char *data, uint16_t len) {
	const char *p = data;
	uint8_t checksum = 0;
	while (p < data + len)
		checksum += *(p++);

	return checksum;
}

const unsigned char *mxm_parse_struct_output(const unsigned char *data) {
	uint8_t is_analog_tv = 0;
	uint8_t is_digital = 0;
	uint8_t is_lvds = 0;
	puts("Output Device Structure");
	hexdump(data, 8);
	/* Device Type */
	printf("Device Type: ");
	switch (data[0] & 0xf0) {
	case 0x00:
		puts("Analog CRT");
		break;
	case 0x10:
		puts("Analog TV/HDTV");
		is_analog_tv = 1;
		break;
	case 0x20:
		puts("TMDS or HDMI");
		is_digital = 1;
		break;
	case 0x30:
		puts("LVDS");
		is_digital = 1;
		is_lvds = 1;
		break;
	case 0x60:
		puts("DisplayPort");
		is_digital = 1;
		break;
	default:
		puts("Reserved");
	}

	printf("DDC/AUX port: ");
	switch (data[1] & 0x0f) {
		case 0x00: puts("VGA_DDC"); break;
		case 0x01: puts("LVDS_DDC"); break;
		case 0x09: puts("Aux Port for DP_A"); break;
		case 0x0a: puts("Aux Port for DP_B"); break;
		case 0x0b: puts("Aux Port for DP_C"); break;
		case 0x0c: puts("Aux Port for DP_D"); break;
		case 0x0d: puts("Not applicable"); break;
		default:   puts("Reserved");
	}

	printf("Connector Type: ");
	switch ((data[1] & 0xf0)>>4 + ((data[2] & 1)<<4)) {
		case 0x00: puts("VGA"); break;
		case 0x01: puts("LVDS"); break;
		case 0x02: puts("HDMI"); break;
		case 0x03: puts("DVI-D"); break;
		case 0x04: puts("DVI-I Analog Port"); break;
		case 0x05: puts("DVI-I Digital Port"); break;
		case 0x06: puts("DisplayPort external connector"); break;
		case 0x07: puts("DisplayPort internal connector"); break;
		case 0x08: puts("Composite connector on TV_CVBS"); break;
		case 0x09: puts("Composite connector on TV_Y"); break;
		case 0x0a: puts("S-video connector on TV_C and TV_Y"); break;
		case 0x0b: puts("HDTV connector on HDTV_Y, HDTV_Pr, HDTV_Pb"); break;
		case 0x0c: puts("Reserved"); break;
		case 0x0d: puts("HDTV connector on HDTV_R, HDTV_G, HDTV_B"); break;
		case 0x0e: puts("eDP"); break;
		case 0x1f: puts("Not applicable"); break;
		default:   puts("Reserved");
	}

	printf("Connector Location: ");
	switch ((data[2] >> 1) & 0x03) {
		case 0x00: puts("Internal"); break;
		case 0x01: puts("Chassis"); break;
		case 0x02: puts("Dock"); break;
		case 0x03: puts("Chassis, not available when docked"); break;
	}

	if (is_digital) {
		printf("Digital Connection: ");
		switch ((data[2] >> 3) & 0xf) {
			case 0x01: puts("Single Link TMDS (over LVDS)"); break;
			case 0x02: puts("Dual Link TMDS (DP_A+DP_B)"); break;
			case 0x03: puts("Dual Link TMDS (DP_A+DP_C)"); break;
			case 0x04: puts("Dual Link TMDS (DP_C+DP_D)"); break;
			case 0x05: puts("Dual Link TMDS (LVDS)"); break;
			case 0x06: puts("Single Link LVDS"); break;
			case 0x07: puts("Dual Link LVDS"); break;
			case 0x0a: puts("DisplayPort DP_A"); break;
			case 0x0b: puts("DisplayPort DP_B"); break;
			case 0x0c: puts("DisplayPort DP_C"); break;
			case 0x0d: puts("DisplayPort DP_D"); break;
			case 0x0f: puts("Not applicable"); break;
			default:   puts("Reserved");
		}

		printf("Digital Audio Connection: ");
		switch ((data[2]&0x80)>7 | ((data[3]&1) <<1)) {
			case 0x00: puts("SPDIF"); break;
			case 0x01: puts("HDA"); break;
			case 0x02: puts("PCIe bus"); break;
			default:   puts("No audio");
		}

		printf("Spread Spectrum: ");
		data[3] & 0x2 ? puts("Enabled") : puts("Disabled");

		printf("CEC: ");
		data[3] & 0x4 ? puts("Disabled") : puts("Enabled");
	}

	/* TODO: Analog TV default format */

	if (is_lvds) {
		printf("Default LVDS Width: ");
		data[3] & 0x8 ? puts("18-bit") : puts("24-bit");
	}

	printf("GPIO for Output select: ");
	int pin = ((data[3] & 0xf0) >> 4) + ((data[4] & 1) << 4);
	if (pin == 0x1f) {
		puts("unused");
	} else {
		printf("%d, ", pin);
		printf("logical %c selects\n",
		       data[4] & 2 ? '1' : '0');
	}

	printf("System Output Method: ");
	data[4] & 4 ? puts("int15h/EFI/ACPI") : puts("GPIO");

	printf("GPIO for DDC select: ");
	pin = (data[4] & 0xf8) >> 3;
	if (pin == 0x1f)
		puts("unused");
	else
		printf("%d, ", pin);

	printf("System DDC Method: ");
	data[5] & 0x1 ? puts("int15/ACPI") : puts("GPIO");

	printf("GPIO for Device Detection: ");
	pin = (data[5] & 0x3e) >> 1;
	if (pin == 0x1f) {
		puts("unused");
	} else {
		printf("%d, ", pin);
		printf("logical %c selects\n",
		       data[5] & 0x40 ? '1' : '0');
	}

	printf("System Hot Plug Notify: ");
	data[5] & 0x80 ? puts("ACPI") : puts("no");

	if (is_lvds) {
		printf("LVDS Type: ");
		switch ((data[6] & 0xe0) >> 5) {
		case 0:
			puts("SPWG");
			break;
		case 1:
			puts("Open LDI");
			break;
		default:
			puts("Reserved");
		}
	}

	return data+8;
}

const unsigned char *mxm_parse_struct_cooling(const unsigned char *data) {
	puts("System Cooling Capability Structure");
	hexdump(data, 4);

	switch (data[0] & 0xf0) {
	case 0x00:
		printf("Maximum cooling capability for the entire module: ");
		break;
	default:
		printf("Unknown: ");
	}

	int cap = (data[1] + ((data[2] & 0x0f) << 8));
	printf("%u.%u W\n", cap/10, cap%10);

	return data+4;
}

const unsigned char *mxm_parse_struct_thermal(const unsigned char *data) {
	puts("Thermal Structure");
	hexdump(data, 4);

	switch (data[0] & 0xf0) {
	case 0x00:
		printf("Maximum temperature for module to maintain: ");
		break;
	case 0x10:
		printf("Temperature to assert MXM TH_ALERT signal: ");
		break;
	default:
		printf("Unknown: ");
	}

	int temp = (data[1] + ((data[2] & 0x07) << 8));
	printf("%u.%u°C\n", temp/10, temp%10);

	return data+4;
}

const unsigned char *mxm_parse_struct_power(const unsigned char *data) {
	puts("Input Power Structure");
	hexdump(data, 4);

	int type = (data[0] & 0xf0) >> 4;
	switch (type) {
		case 0x00: printf("When PWR_LEVEL# asserted: "); break;
		case 0x01: printf("When PWR_LEVEL# not asserted: "); break;
		case 0x09: printf("P1: "); break;
		case 0x0a: printf("P2: "); break;
		case 0x0b: printf("P3: "); break;
		case 0x0c: printf("P4: "); break;
		default:   printf("Unknown: ");
	}
	int power = (data[2] + ((data[3] & 0x0f) << 8));
	printf("%u.%u W\n", power/10, power%10);

	if (!type) {
		printf("PWR_LEVEL# pin will%s stay asserted during this state\n",
		       data[1] & 1 ? "" : " not");
	}

	printf("Software Notification is%s used\n",
	      data[1] & 2 ? " not" : "");

	return data+4;
}

const unsigned char *mxm_parse_struct_vendor(const unsigned char *data) {
	puts("Vendor Specific Structure");
	hexdump(data, 8);

	uint16_t vid = (data[0]&0xf0)>>4;
	vid += data[1]<<4;
	vid += (data[2]&0x0f)<<12;
	printf("VID: %04x\n", vid);

	return data+8;
}

const unsigned char *mxm_parse_struct_backlight(const unsigned char *data) {
	puts("Backlight Control Structure");
	hexdump(data, 8);

	printf("Output Device Index: %u\n", (data[0]&0xf0)>>4);

	printf("Type: ");
	switch ((data[1]&0xc)>>2) {
		case 0x00: puts("CCFL"); break;
		case 0x01: puts("LED"); break;
		default: puts("Unknown"); break;
	}

	printf("Control Type: ");
	switch (data[1]&0x3) {
	case 0x00: /* PWM */
		puts("PWM");
		break;
	case 0x01: /* SMBus */
		printf("SMBus, address %02x, id %02x\n",
				data[2], data[3]);
		break;
	default:   /* Unknown */
		puts("Unknown");
	}

	int entries = (data[1]&0xf0)>>4;

	data += 4;
	while (entries--) {
		puts("\nBacklight Frequency Structure");
		hexdump(data, 8);

		uint32_t f = data[0] + (data[1]<<8) + ((data[2]&1)<<16);
		printf("Frequency: %u.%u Hz\n", f/10, f%10);

		uint32_t max = data[4] + ((data[5]&3)<<8);
		printf("Maximum Duty Cycle at this frequency: %u.%u%\n",
		       max/10, max%10);

		uint32_t min = ((data[5]&0xfc)>>2) + ((data[6]&0x0f)<<6);
		printf("Minimum Duty Cycle at this frequency. %u.%u%\n",
		       min/10, min%10);

		data += 8;
	}

	return data;
}

const unsigned char *mxm_parse_struct(const unsigned char *data) {
	uint8_t descriptor = *data & 0x0f;
	switch (descriptor) {
	case 0x00:
		return mxm_parse_struct_output(data);
	case 0x01:
		return mxm_parse_struct_cooling(data);
	case 0x02:
		return mxm_parse_struct_thermal(data);
	case 0x03:
		return mxm_parse_struct_power(data);
	case 0x04:
		fprintf(stderr, "GPIO Not implemented yet!\n");
		return NULL;
	case 0x05:
		return mxm_parse_struct_vendor(data);
	case 0x06:
		return mxm_parse_struct_backlight(data);
	default:
		fprintf(stderr, "Unknown type 0x%02x!\n", descriptor);
		return NULL;
	}
}

/* Returns length, including the header and checksum.
 * Negative values signal errors. */
unsigned int mxm_parse_sis(const unsigned char *data) {
	if (memcmp("MXM_", data, 4)) {
		fprintf(stderr, "Error: Invalid magic string\n");
		return -1;
	}
	/* length, including the header and checksum */
	uint16_t len = *(data+6) + 8;
	if (checksum(data, len)) {
		fprintf(stderr, "Error: Invalid checksum!\n");
		return -2;
	}
	printf("Version %u.%u, %u bytes.\n",
	       data[4], data[5], len);

	if (data[4] != 0x03) {
		printf("Unsupported version, skipping parsing.");
		return -4;
	}

	const unsigned char *p = data + 8;
	while (p < data + len - 1 && p) {
		putchar('\n');
		p = mxm_parse_struct(p);
	}
	putchar('\n');

	return 0;
}
