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

#include <cpuinfo.h>
#include <cpuinfo/common.h>
#include <x86/api.h>


/* The state of the parser to be preserved between parsing different tokens. */
struct parser_state {
	/*
	 * Pointer to the start of the previous token if it is "model".
	 * NULL if previous token is not "model".
	 */
	char* context_model;
	/*
	 * Pointer to the start of the previous token if it is a single-uppercase-letter token.
	 * NULL if previous token is anything different.
	 */
	char* context_upper_letter;
	/*
	 * Pointer to the start of the previous token if it is "Dual".
	 * NULL if previous token is not "Dual".
	 */
	char* context_dual;
	/*
	 * Pointer to the start of the previous token if it is "Core", "Dual-Core", "QuadCore", etc.
	 * NULL if previous token is anything different.
	 */
	char* context_core;
	/*
	 * Pointer to the start of the previous token if it is "Eng" or "Engineering", etc.
	 * NULL if previous token is anything different.
	 */
	char* context_engineering;
	/*
	 * Pointer to the '@' symbol in the brand string (separates frequency specification).
	 * NULL if there is no '@' symbol.
	 */
	char* frequency_separator;
	/* Indicates whether the brand string (after transformations) contains frequency. */
	bool frequency_token;
	/* Indicates whether the processor is of Xeon family (contains "Xeon" substring). */
	bool xeon;
	/* Indicates whether the processor model number was already parsed. */
	bool parsed_model_number;
	/* Indicates whether the processor is an engineering sample (contains "Engineering Sample" or "Eng Sample" substrings). */
	bool engineering_sample;
};

/** @brief	Resets information about the previous token. Keeps all other state information. */
static void reset_context(struct parser_state* state) {
	state->context_model = NULL;
	state->context_upper_letter = NULL;
	state->context_dual = NULL;
	state->context_core = NULL;
}

/**
 * @brief	Overwrites the supplied string with space characters if it exactly matches the given string.
 * @param	string	The string to be compared against other string, and erased in case of matching.
 * @param	length	The length of the two string to be compared against each other.
 * @param	target	The string to compare against.
 * @retval	true	If the two strings match and the first supplied string was erased (overwritten with space characters).
 * @retval	false	If the two strings are different and the first supplied string remained unchanged.
 */
static inline bool erase_matching(char* string, size_t length, const char* target) {
	const bool match = memcmp(string, target, length) == 0;
	if (match) {
		memset(string, ' ', length);
	}
	return match;
}

/**
 * @brief	Checks if the supplied ASCII character is an uppercase latin letter.
 * @param	character	The character to analyse.
 * @retval	true	If the supplied character is an uppercase latin letter ('A' to 'Z').
 * @retval	false	If the supplied character is anything different.
 */
static inline bool is_upper_letter(char character) {
	return (uint32_t) (character - 'A') <= (uint32_t)('Z' - 'A');
}

/**
 * @brief	Checks if the supplied ASCII character is a digit.
 * @param	character	The character to analyse.
 * @retval	true	If the supplied character is a digit ('0' to '9').
 * @retval	false	If the supplied character is anything different.
 */
static inline bool is_digit(char character) {
	return (uint32_t) (character - '0') < UINT32_C(10);
}

static inline bool is_zero_number(const char* token_start, const char* token_end) {
	for (const char* char_ptr = token_start; char_ptr != token_end; char_ptr++) {
		if (*char_ptr != '0') {
			return false;
		}
	}
	return true;
}

static inline bool is_space(const char* token_start, const char* token_end) {
	for (const char* char_ptr = token_start; char_ptr != token_end; char_ptr++) {
		if (*char_ptr != ' ') {
			return false;
		}
	}
	return true;
}

static inline bool is_number(const char* token_start, const char* token_end) {
	for (const char* char_ptr = token_start; char_ptr != token_end; char_ptr++) {
		if (!is_digit(*char_ptr)) {
			return false;
		}
	}
	return true;
}

static inline bool is_model_number(const char* token_start, const char* token_end) {
	for (const char* char_ptr = token_start + 1; char_ptr < token_end; char_ptr++) {
		if (is_digit(char_ptr[-1]) && is_digit(char_ptr[0])) {
			return true;
		}
	}
	return false;
}

static inline bool is_frequency(const char* token_start, const char* token_end) {
	const size_t token_length = (size_t) (token_end - token_start);
	if (token_length > 3 && token_end[-2] == 'H' && token_end[-1] == 'z') {
		switch (token_end[-3]) {
			case 'K':
			case 'M':
			case 'G':
				return true;
		}
	}
	return false;
}

/**
 * @warning	Input and output tokens can overlap
 */
static inline char* move_token(const char* token_start, const char* token_end, char* output_ptr) {
	const size_t token_length = (size_t) (token_end - token_start);
	memmove(output_ptr, token_start, token_length);
	return output_ptr + token_length;
}

static bool transform_token(char* token_start, char* token_end, struct parser_state* state) {
	const struct parser_state previousState = *state;
	reset_context(state);

	size_t token_length = (size_t) (token_end - token_start);

	if (state->frequency_separator != NULL) {
		if (token_start > state->frequency_separator) {
			if (state->parsed_model_number) {
				memset(token_start, ' ', token_length);
			}
		}
	}


	/* Early AMD and Cyrix processors have "tm" suffix for trademark, e.g.
	 *   "AMD-K6tm w/ multimedia extensions"
	 *   "Cyrix MediaGXtm MMXtm Enhanced"
	 */
	if (token_length > 2) {
		const char context_char = token_end[-3];
		if (is_digit(context_char) || is_upper_letter(context_char)) {
			if (erase_matching(token_end - 2, 2, "tm")) {
				token_end -= 2;
				token_length -= 2;
			}
		}
	}
	if (token_length > 4) {
		/* Some early AMD CPUs have "AMD-" at the beginning, e.g.
		 *   "AMD-K5(tm) Processor"
		 *   "AMD-K6tm w/ multimedia extensions"
		 *   "AMD-K6(tm) 3D+ Processor"
		 *   "AMD-K6(tm)-III Processor"
		 */
		if (erase_matching(token_start, 4, "AMD-")) {
			token_start += 4;
			token_length -= 4;
		}
	}
	switch (token_length) {
		case 1:
			/*
			 * On some Intel processors there is a space between the first letter of
			 * the name and the number after it, e.g.
			 *   "Intel(R) Core(TM) i7 CPU X 990  @ 3.47GHz"
			 *   "Intel(R) Core(TM) CPU Q 820  @ 1.73GHz"
			 * We want to merge these parts together, in reverse order, i.e. "X 990" -> "990X", "820" -> "820Q"
			 */
			if (is_upper_letter(token_start[0])) {
				state->context_upper_letter = token_start;
				return true;
			}
			break;
		case 2:
			/* Erase everything after "w/" in "AMD-K6tm w/ multimedia extensions" */
			if (erase_matching(token_start, token_length, "w/")) {
				return false;
			}
			/*
			 * Intel Xeon processors since Ivy Bridge use versions, e.g.
			 *   "Intel Xeon E3-1230 v2"
			 * Some processor branch strings report them as "V<N>", others report as "v<N>".
			 * Normalize the former (upper-case) to the latter (lower-case) version
			 */
			if (token_start[0] == 'V' && is_digit(token_start[1])) {
				token_start[0] = 'v';
				return true;
			}
			break;
		case 3:
			/*
			 * Erase "CPU" in brand string on Intel processors, e.g.
			 *  "Intel(R) Core(TM) i5 CPU         650  @ 3.20GHz"
			 *  "Intel(R) Xeon(R) CPU           X3210  @ 2.13GHz"
			 *  "Intel(R) Atom(TM) CPU Z2760  @ 1.80GHz"
			 */
			if (erase_matching(token_start, token_length, "CPU")) {
				return true;
			}
			/*
			 * Erase everything after "SOC" on AMD System-on-Chips, e.g.
			 *  "AMD GX-212JC SOC with Radeon(TM) R2E Graphics  \0"
			 */
			if (erase_matching(token_start, token_length, "SOC")) {
				return false;
			}
			/*
			 * Erase "AMD" in brand string on AMD processors, e.g.
			 *  "AMD Athlon(tm) Processor"
			 *  "AMD Engineering Sample"
			 *  "Quad-Core AMD Opteron(tm) Processor 2344 HE"
			 */
			if (erase_matching(token_start, token_length, "AMD")) {
				return true;
			}
			/*
			 * Erase "VIA" in brand string on VIA processors, e.g.
			 *   "VIA C3 Ezra"
			 *   "VIA C7-M Processor 1200MHz"
			 *   "VIA Nano L3050@1800MHz"
			 */
			if (erase_matching(token_start, token_length, "VIA")) {
				return true;
			}
			/* Erase "IDT" in brand string on early Centaur processors, e.g. "IDT WinChip 2-3D" */
			if (erase_matching(token_start, token_length, "IDT")) {
				return true;
			}
			/*
			 * Erase everything starting with "MMX" in
			 * "Cyrix MediaGXtm MMXtm Enhanced" ("tm" suffix is removed by this point)
			 */
			if (erase_matching(token_start, token_length, "MMX")) {
				return false;
			}
			/*
			 * Erase everything starting with "APU" on AMD processors, e.g.
			 *   "AMD A10-4600M APU with Radeon(tm) HD Graphics"
			 *   "AMD A10-7850K APU with Radeon(TM) R7 Graphics"
			 *   "AMD A6-6310 APU with AMD Radeon R4 Graphics"
			 */
			if (erase_matching(token_start, token_length, "APU")) {
				return false;
			}
			/*
			 * Remember to discard string if it contains "Eng Sample",
			 * e.g. "Eng Sample, ZD302046W4K43_36/30/20_2/8_A"
			 */
			if (memcmp(token_start, "Eng", token_length) == 0) {
				state->context_engineering = token_start;
			}
			break;
		case 4:
			/* Remember to erase "Dual Core" in "AMD Athlon(tm) 64 X2 Dual Core Processor 3800+" */
			if (memcmp(token_start, "Dual", token_length) == 0) {
				state->context_dual = token_start;
			}
			/* Remember if the processor is on Xeon family */
			if (memcmp(token_start, "Xeon", token_length) == 0) {
				state->xeon = true;
			}
			/* Erase "Dual Core" in "AMD Athlon(tm) 64 X2 Dual Core Processor 3800+" */
			if (previousState.context_dual != NULL) {
				if (memcmp(token_start, "Core", token_length) == 0) {
					memset(previousState.context_dual, ' ', (size_t) (token_end - previousState.context_dual));
					state->context_core = token_end;
					return true;
				}
			}
			break;
		case 5:
			/*
			 * Erase "Intel" in brand string on Intel processors, e.g.
			 *   "Intel(R) Xeon(R) CPU X3210 @ 2.13GHz"
			 *   "Intel(R) Atom(TM) CPU D2700 @ 2.13GHz"
			 *   "Genuine Intel(R) processor 800MHz"
			 */
			if (erase_matching(token_start, token_length, "Intel")) {
				return true;
			}
			/*
			 * Erase "Cyrix" in brand string on Cyrix processors, e.g.
			 *   "Cyrix MediaGXtm MMXtm Enhanced"
			 */
			if (erase_matching(token_start, token_length, "Cyrix")) {
				return true;
			}
			/*
			 * Erase everything following "Geode" (but not "Geode" token itself) on Geode processors, e.g.
			 *   "Geode(TM) Integrated Processor by AMD PCS"
			 *   "Geode(TM) Integrated Processor by National Semi"
			 */
			if (memcmp(token_start, "Geode", token_length) == 0) {
				return false;
			}
			/* Remember to erase "model unknown" in "AMD Processor model unknown" */
			if (memcmp(token_start, "model", token_length) == 0) {
				state->context_model = token_start;
				return true;
			}
			break;
		case 6:
			/*
			 * Erase everything starting with "Radeon" or "RADEON" on AMD APUs, e.g.
			 *   "A8-7670K Radeon R7, 10 Compute Cores 4C+6G"
			 *   "FX-8800P Radeon R7, 12 Compute Cores 4C+8G"
			 *   "A12-9800 RADEON R7, 12 COMPUTE CORES 4C+8G"
			 *   "A9-9410 RADEON R5, 5 COMPUTE CORES 2C+3G"
			 */
			if (erase_matching(token_start, token_length, "Radeon") || erase_matching(token_start, token_length, "RADEON")) {
				return false;
			}
			/*
			 * Erase "Mobile" when it is not part of the processor name,
			 * e.g. in "AMD Turion(tm) X2 Ultra Dual-Core Mobile ZM-82"
			 */
			if (previousState.context_core != NULL) {
				if (erase_matching(token_start, token_length, "Mobile")) {
					return true;
				}
			}
			/* Erase "family" in "Intel(R) Pentium(R) III CPU family 1266MHz" */
			if (erase_matching(token_start, token_length, "family")) {
				return true;
			}
			/* Discard the string if it contains "Engineering Sample" */
			if (previousState.context_engineering != NULL) {
				if (memcmp(token_start, "Sample", token_length) == 0) {
					state->engineering_sample = true;
					return false;
				}
			}
			break;
		case 7:
			/*
			 * Erase "Geniune" in brand string on Intel engineering samples, e.g.
			 *   "Genuine Intel(R) processor 800MHz"
			 *   "Genuine Intel(R) CPU @ 2.13GHz"
			 *   "Genuine Intel(R) CPU 0000 @ 1.73GHz"
			 */
			if (erase_matching(token_start, token_length, "Genuine")) {
				return true;
			}
			/*
			 * Erase "12-core" in brand string on AMD Threadripper, e.g.
			 *   "AMD Ryzen Threadripper 1920X 12-Core Processor"
			 */
			if (erase_matching(token_start, token_length, "12-Core")) {
				return true;
			}
			/*
			 * Erase "16-core" in brand string on AMD Threadripper, e.g.
			 *   "AMD Ryzen Threadripper 1950X 16-Core Processor"
			 */
			if (erase_matching(token_start, token_length, "16-Core")) {
				return true;
			}
			/* Erase "model unknown" in "AMD Processor model unknown" */
			if (previousState.context_model != NULL) {
				if (memcmp(token_start, "unknown", token_length) == 0) {
					memset(previousState.context_model, ' ', token_end - previousState.context_model);
					return true;
				}
			}
			/*
			 * Discard the string if it contains "Eng Sample:" or "Eng Sample," e.g.
			 *   "AMD Eng Sample, ZD302046W4K43_36/30/20_2/8_A"
			 *   "AMD Eng Sample: 2D3151A2M88E4_35/31_N"
			 */
			if (previousState.context_engineering != NULL) {
				if (memcmp(token_start, "Sample,", token_length) == 0 || memcmp(token_start, "Sample:", token_length) == 0) {
					state->engineering_sample = true;
					return false;
				}
			}
			break;
		case 8:
			/* Erase "QuadCore" in "VIA QuadCore L4700 @ 1.2+ GHz" */
			if (erase_matching(token_start, token_length, "QuadCore")) {
				state->context_core = token_end;
				return true;
			}
			/* Erase "Six-Core" in "AMD FX(tm)-6100 Six-Core Processor" */
			if (erase_matching(token_start, token_length, "Six-Core")) {
				state->context_core = token_end;
				return true;
			}
			break;
		case 9:
			if (erase_matching(token_start, token_length, "Processor")) {
				return true;
			}
			if (erase_matching(token_start, token_length, "processor")) {
				return true;
			}
			/* Erase "Dual-Core" in "Pentium(R) Dual-Core CPU T4200 @ 2.00GHz" */
			if (erase_matching(token_start, token_length, "Dual-Core")) {
				state->context_core = token_end;
				return true;
			}
			/* Erase "Quad-Core" in AMD processors, e.g.
			 *   "Quad-Core AMD Opteron(tm) Processor 2347 HE"
			 *   "AMD FX(tm)-4170 Quad-Core Processor"
			 */
			if (erase_matching(token_start, token_length, "Quad-Core")) {
				state->context_core = token_end;
				return true;
			}
			/* Erase "Transmeta" in brand string on Transmeta processors, e.g.
			 *   "Transmeta(tm) Crusoe(tm) Processor TM5800"
			 *   "Transmeta Efficeon(tm) Processor TM8000"
			 */
			if (erase_matching(token_start, token_length, "Transmeta")) {
				return true;
			}
			break;
		case 10:
			/*
			 * Erase "Eight-Core" in AMD processors, e.g.
			 *   "AMD FX(tm)-8150 Eight-Core Processor"
			 */
			if (erase_matching(token_start, token_length, "Eight-Core")) {
				state->context_core = token_end;
				return true;
			}
			break;
		case 11:
			/*
			 * Erase "Triple-Core" in AMD processors, e.g.
			 *   "AMD Phenom(tm) II N830 Triple-Core Processor"
			 *   "AMD Phenom(tm) 8650 Triple-Core Processor"
			 */
			if (erase_matching(token_start, token_length, "Triple-Core")) {
				state->context_core = token_end;
				return true;
			}
			/*
			 * Remember to discard string if it contains "Engineering Sample",
			 * e.g. "AMD Engineering Sample"
			 */
			if (memcmp(token_start, "Engineering", token_length) == 0) {
				state->context_engineering = token_start;
				return true;
			}
			break;
	}
	if (is_zero_number(token_start, token_end)) {
		memset(token_start, ' ', token_length);
		return true;
	}
	/* On some Intel processors the last letter of the name is put before the number,
	 * and an additional space it added, e.g.
	 *   "Intel(R) Core(TM) i7 CPU X 990  @ 3.47GHz"
	 *   "Intel(R) Core(TM) CPU Q 820  @ 1.73GHz"
	 *   "Intel(R) Core(TM) i5 CPU M 480  @ 2.67GHz"
	 * We fix this issue, i.e. "X 990" -> "990X", "Q 820" -> "820Q"
	 */
	if (previousState.context_upper_letter != 0) {
		/* A single letter token followed by 2-to-5 digit letter is merged together */
		switch (token_length) {
			case 2:
			case 3:
			case 4:
			case 5:
				if (is_number(token_start, token_end)) {
					/* Load the previous single-letter token */
					const char letter = *previousState.context_upper_letter;
					/* Erase the previous single-letter token */
					*previousState.context_upper_letter = ' ';
					/* Move the current token one position to the left */
					move_token(token_start, token_end, token_start - 1);
					token_start -= 1;
					/*
					 * Add the letter on the end
					 * Note: accessing token_start[-1] is safe because this is not the first token
					 */
					token_end[-1] = letter;
				}
		}
	}
	if (state->frequency_separator != NULL) {
		if (is_model_number(token_start, token_end)) {
			state->parsed_model_number = true;
		}
	}
	if (is_frequency(token_start, token_end)) {
		state->frequency_token = true;
	}
	return true;
}

uint32_t cpuinfo_x86_normalize_brand_string(
	const char raw_name[48],
	char normalized_name[48])
{
	normalized_name[0] = '\0';
	char name[48];
	memcpy(name, raw_name, sizeof(name));

	/*
	 * First find the end of the string
	 * Start search from the end because some brand strings contain zeroes in the middle
	 */
	char* name_end = &name[48];
	while (name_end[-1] == '\0') {
		/*
		 * Adject name_end by 1 position and check that we didn't reach the start of the brand string.
		 * This is possible if all characters are zero.
		 */
		if (--name_end == name) {
			/* All characters are zeros */
			return 0;
		}
	}

	struct parser_state parser_state = { 0 };

	/* Now unify all whitespace characters: replace tabs and '\0' with spaces */
	{
		bool inside_parentheses = false;
		for (char* char_ptr = name; char_ptr != name_end; char_ptr++) {
			switch (*char_ptr) {
				case '(':
					inside_parentheses = true;
					*char_ptr = ' ';
					break;
				case ')':
					inside_parentheses = false;
					*char_ptr = ' ';
					break;
				case '@':
					parser_state.frequency_separator = char_ptr;
				case '\0':
				case '\t':
					*char_ptr = ' ';
					break;
				default:
					if (inside_parentheses) {
						*char_ptr = ' ';
					}
			}
		}
	}

	/* Iterate through all tokens and erase redundant parts */
	{
		bool is_token = false;
		char* token_start;
		for (char* char_ptr = name; char_ptr != name_end; char_ptr++) {
			if (*char_ptr == ' ') {
				if (is_token) {
					is_token = false;
					if (!transform_token(token_start, char_ptr, &parser_state)) {
						name_end = char_ptr;
						break;
					}
				}
			} else {
				if (!is_token) {
					is_token = true;
					token_start = char_ptr;
				}
			}
		}
		if (is_token) {
			transform_token(token_start, name_end, &parser_state);
		}
	}

	/* If this is an engineering sample, return empty string */
	if (parser_state.engineering_sample) {
		return 0;
	}

	/* Check if there is some string before the frequency separator. */
	if (parser_state.frequency_separator != NULL) {
		if (is_space(name, parser_state.frequency_separator)) {
			/* If only frequency is available, return empty string */
			return 0;
		}
	}

	/* Compact tokens: collapse multiple spacing into one */
	{
		char* output_ptr = normalized_name;
		char* token_start;
		bool is_token = false;
		bool previous_token_ends_with_dash = true;
		bool current_token_starts_with_dash = false;
		uint32_t token_count = 1;
		for (char* char_ptr = name; char_ptr != name_end; char_ptr++) {
			const char character = *char_ptr;
			if (character == ' ') {
				if (is_token) {
					is_token = false;
					if (!current_token_starts_with_dash && !previous_token_ends_with_dash) {
						token_count += 1;
						*output_ptr++ = ' ';
					}
					output_ptr = move_token(token_start, char_ptr, output_ptr);
					/* Note: char_ptr[-1] exists because there is a token before this space */
					previous_token_ends_with_dash = (char_ptr[-1] == '-');
				}
			} else {
				if (!is_token) {
					is_token = true;
					token_start = char_ptr;
					current_token_starts_with_dash = (character == '-');
				}
			}
		}
		if (is_token) {
			if (!current_token_starts_with_dash && !previous_token_ends_with_dash) {
				token_count += 1;
				*output_ptr++ = ' ';
			}
			output_ptr = move_token(token_start, name_end, output_ptr);
		}
		if (parser_state.frequency_token && token_count <= 1) {
			/* The only remaining part is frequency */
			normalized_name[0] = '\0';
			return 0;
		}
		if (output_ptr < &normalized_name[48]) {
			*output_ptr = '\0';
		} else {
			normalized_name[47] = '\0';
		}
		return (uint32_t) (output_ptr - normalized_name);
	}
}

static const char* vendor_string_map[] = {
	[cpuinfo_vendor_intel] = "Intel",
	[cpuinfo_vendor_amd] = "AMD",
	[cpuinfo_vendor_via] = "VIA",
	[cpuinfo_vendor_hygon] = "Hygon",
	[cpuinfo_vendor_rdc] = "RDC",
	[cpuinfo_vendor_dmp] = "DM&P",
	[cpuinfo_vendor_transmeta] = "Transmeta",
	[cpuinfo_vendor_cyrix] = "Cyrix",
	[cpuinfo_vendor_rise] = "Rise",
	[cpuinfo_vendor_nsc] = "NSC",
	[cpuinfo_vendor_sis] = "SiS",
	[cpuinfo_vendor_nexgen] = "NexGen",
	[cpuinfo_vendor_umc] = "UMC",
};

uint32_t cpuinfo_x86_format_package_name(
	enum cpuinfo_vendor vendor,
	const char normalized_brand_string[48],
	char package_name[CPUINFO_PACKAGE_NAME_MAX])
{
	if (normalized_brand_string[0] == '\0') {
		package_name[0] = '\0';
		return 0;
	}

	const char* vendor_string = NULL;
	if ((uint32_t) vendor < (uint32_t) CPUINFO_COUNT_OF(vendor_string_map)) {
		vendor_string = vendor_string_map[(uint32_t) vendor];
	}
	if (vendor_string == NULL) {
		strncpy(package_name, normalized_brand_string, CPUINFO_PACKAGE_NAME_MAX);
		package_name[CPUINFO_PACKAGE_NAME_MAX - 1] = '\0';
		return 0;
	} else {
		snprintf(package_name, CPUINFO_PACKAGE_NAME_MAX,
			"%s %s", vendor_string, normalized_brand_string);
		return (uint32_t) strlen(vendor_string) + 1;
	}
}
