#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <sched.h>

#if CPUINFO_MOCK
	#include <cpuinfo-mock.h>
#endif
#include <linux/api.h>
#include <cpuinfo/log.h>


/*
 * Size, in chars, of the on-stack buffer used for parsing cpu lists.
 * This is also the limit on the length of a single entry
 * (<cpu-number> or <cpu-number-start>-<cpu-number-end>)
 * in the cpu list.
 */
#define BUFFER_SIZE 256


/* Locale-independent */
inline static bool is_whitespace(char c) {
	switch (c) {
		case ' ':
		case '\t':
		case '\n':
		case '\r':
			return true;
		default:
			return false;
	}
}

inline static const char* parse_number(const char* string, const char* end, uint32_t number_ptr[restrict static 1]) {
	uint32_t number = 0;
	while (string != end) {
		const uint32_t digit = (uint32_t) (*string) - (uint32_t) '0';
		if (digit >= 10) {
			break;
		}
		number = number * UINT32_C(10) + digit;
		string += 1;
	}
	*number_ptr = number;
	return string;
}

inline static bool parse_entry(const char* entry_start, const char* entry_end, cpuinfo_cpulist_callback callback, void* context) {
	/* Skip whitespace at the beginning of an entry */
	for (; entry_start != entry_end; entry_start++) {
		if (!is_whitespace(*entry_start)) {
			break;
		}
	}
	/* Skip whitespace at the end of an entry */
	for (; entry_end != entry_start; entry_end--) {
		if (!is_whitespace(entry_end[-1])) {
			break;
		}
	}

	const size_t entry_length = (size_t) (entry_end - entry_start);
	if (entry_length == 0) {
		cpuinfo_log_warning("unexpected zero-length cpu list entry ignored");
		return false;
	}

	#if CPUINFO_LOG_DEBUG_PARSERS
		cpuinfo_log_debug("parse cpu list entry \"%.*s\" (%zu chars)", (int) entry_length, entry_start, entry_length);
	#endif
	uint32_t first_cpu, last_cpu;

	const char* number_end = parse_number(entry_start, entry_end, &first_cpu);
	if (number_end == entry_start) {
		/* Failed to parse the number; ignore the entry */
		cpuinfo_log_warning("invalid character '%c' in the cpu list entry \"%.*s\": entry is ignored",
			entry_start[0], (int) entry_length, entry_start);
		return false;
	} else if (number_end == entry_end) {
		/* Completely parsed the entry */
		#if CPUINFO_LOG_DEBUG_PARSERS
			cpuinfo_log_debug("cpulist: call callback with list_start = %"PRIu32", list_end = %"PRIu32,
				first_cpu, first_cpu + 1);
		#endif
		return callback(first_cpu, first_cpu + 1, context);
	}

	/* Parse the second part of the entry */
	if (*number_end != '-') {
		cpuinfo_log_warning("invalid character '%c' in the cpu list entry \"%.*s\": entry is ignored",
			*number_end, (int) entry_length, entry_start);
		return false;
	}

	const char* number_start = number_end + 1;
	number_end = parse_number(number_start, entry_end, &last_cpu);
	if (number_end == number_start) {
		/* Failed to parse the second number; ignore the entry */
		cpuinfo_log_warning("invalid character '%c' in the cpu list entry \"%.*s\": entry is ignored",
			*number_start, (int) entry_length, entry_start);
		return false;
	}

	if (number_end != entry_end) {
		/* Partially parsed the entry; ignore unparsed characters and continue with the parsed part */
		cpuinfo_log_warning("ignored invalid characters \"%.*s\" at the end of cpu list entry \"%.*s\"",
			(int) (entry_end - number_end), number_start, (int) entry_length, entry_start);
	}

	if (last_cpu < first_cpu) {
		cpuinfo_log_warning("ignored cpu list entry \"%.*s\": invalid range %"PRIu32"-%"PRIu32,
			(int) entry_length, entry_start, first_cpu, last_cpu);
		return false;
	}

	/* Parsed both parts of the entry; update CPU set */
	#if CPUINFO_LOG_DEBUG_PARSERS
		cpuinfo_log_debug("cpulist: call callback with list_start = %"PRIu32", list_end = %"PRIu32,
			first_cpu, last_cpu + 1);
	#endif
	return callback(first_cpu, last_cpu + 1, context);
}

bool cpuinfo_linux_parse_cpulist(const char* filename, cpuinfo_cpulist_callback callback, void* context) {
	bool status = true;
	int file = -1;
	char buffer[BUFFER_SIZE];
	#if CPUINFO_LOG_DEBUG_PARSERS
		cpuinfo_log_debug("parsing cpu list from file %s", filename);
	#endif

#if CPUINFO_MOCK
	file = cpuinfo_mock_open(filename, O_RDONLY);
#else
	file = open(filename, O_RDONLY);
#endif
	if (file == -1) {
		cpuinfo_log_info("failed to open %s: %s", filename, strerror(errno));
		status = false;
		goto cleanup;
	}

	size_t position = 0;
	const char* buffer_end = &buffer[BUFFER_SIZE];
	char* data_start = buffer;
	ssize_t bytes_read;
	do {
#if CPUINFO_MOCK
		bytes_read = cpuinfo_mock_read(file, data_start, (size_t) (buffer_end - data_start));
#else
		bytes_read = read(file, data_start, (size_t) (buffer_end - data_start));
#endif
		if (bytes_read < 0) {
			cpuinfo_log_info("failed to read file %s at position %zu: %s", filename, position, strerror(errno));
			status = false;
			goto cleanup;
		}

		position += (size_t) bytes_read;
		const char* data_end = data_start + (size_t) bytes_read;
		const char* entry_start = buffer;

		if (bytes_read == 0) {
			/* No more data in the file: process the remaining text in the buffer as a single entry */
			const char* entry_end = data_end;
			const bool entry_status = parse_entry(entry_start, entry_end, callback, context);
			status &= entry_status;
		} else {
			const char* entry_end;
			do {
				/* Find the end of the entry, as indicated by a comma (',') */
				for (entry_end = entry_start; entry_end != data_end; entry_end++) {
					if (*entry_end == ',') {
						break;
					}
				}

				/*
				 * If we located separator at the end of the entry, parse it.
				 * Otherwise, there may be more data at the end; read the file once again.
				 */
				if (entry_end != data_end) {
					const bool entry_status = parse_entry(entry_start, entry_end, callback, context);
					status &= entry_status;
					entry_start = entry_end + 1;
				}
			} while (entry_end != data_end);

			/* Move remaining partial entry data at the end to the beginning of the buffer */
			const size_t entry_length = (size_t) (entry_end - entry_start);
			memmove(buffer, entry_start, entry_length);
			data_start = &buffer[entry_length];
		}
	} while (bytes_read != 0);

cleanup:
	if (file != -1) {
#if CPUINFO_MOCK
		cpuinfo_mock_close(file);
#else
		close(file);
#endif
		file = -1;
	}
	return status;
}
