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

#if !defined(__ANDROID__)
/*
 * sched.h is only used for CPU_SETSIZE constant.
 * Android NDK headers before platform 21 do have this constant in sched.h
 */
#include <sched.h>
#endif

#include <cpuinfo/log.h>
#include <linux/api.h>

#define STRINGIFY(token) #token

#define KERNEL_MAX_FILENAME "/sys/devices/system/cpu/kernel_max"
#define KERNEL_MAX_FILESIZE 32
#define FREQUENCY_FILENAME_SIZE \
	(sizeof("/sys/devices/system/cpu/cpu" STRINGIFY(UINT32_MAX) "/cpufreq/cpuinfo_max_freq"))
#define CUR_FREQUENCY_FILENAME_FORMAT "/sys/devices/system/cpu/cpu%" PRIu32 "/cpufreq/cpuinfo_cur_freq"
#define MAX_FREQUENCY_FILENAME_FORMAT "/sys/devices/system/cpu/cpu%" PRIu32 "/cpufreq/cpuinfo_max_freq"
#define MIN_FREQUENCY_FILENAME_FORMAT "/sys/devices/system/cpu/cpu%" PRIu32 "/cpufreq/cpuinfo_min_freq"
#define FREQUENCY_FILESIZE 32
#define PACKAGE_ID_FILENAME_SIZE \
	(sizeof("/sys/devices/system/cpu/cpu" STRINGIFY(UINT32_MAX) "/topology/physical_package_id"))
#define PACKAGE_ID_FILENAME_FORMAT "/sys/devices/system/cpu/cpu%" PRIu32 "/topology/physical_package_id"
#define PACKAGE_ID_FILESIZE 32
#define CORE_ID_FILENAME_SIZE (sizeof("/sys/devices/system/cpu/cpu" STRINGIFY(UINT32_MAX) "/topology/core_id"))
#define CORE_ID_FILENAME_FORMAT "/sys/devices/system/cpu/cpu%" PRIu32 "/topology/core_id"
#define CORE_ID_FILESIZE 32

#define CORE_CPUS_FILENAME_SIZE (sizeof("/sys/devices/system/cpu/cpu" STRINGIFY(UINT32_MAX) "/topology/core_cpus_list"))
#define CORE_CPUS_FILENAME_FORMAT "/sys/devices/system/cpu/cpu%" PRIu32 "/topology/core_cpus_list"
#define CORE_SIBLINGS_FILENAME_SIZE \
	(sizeof("/sys/devices/system/cpu/cpu" STRINGIFY(UINT32_MAX) "/topology/core_siblings_list"))
#define CORE_SIBLINGS_FILENAME_FORMAT "/sys/devices/system/cpu/cpu%" PRIu32 "/topology/core_siblings_list"
#define CLUSTER_CPUS_FILENAME_SIZE \
	(sizeof("/sys/devices/system/cpu/cpu" STRINGIFY(UINT32_MAX) "/topology/cluster_cpus_list"))
#define CLUSTER_CPUS_FILENAME_FORMAT "/sys/devices/system/cpu/cpu%" PRIu32 "/topology/cluster_cpus_list"
#define PACKAGE_CPUS_FILENAME_SIZE \
	(sizeof("/sys/devices/system/cpu/cpu" STRINGIFY(UINT32_MAX) "/topology/package_cpus_list"))
#define PACKAGE_CPUS_FILENAME_FORMAT "/sys/devices/system/cpu/cpu%" PRIu32 "/topology/package_cpus_list"
#define THREAD_SIBLINGS_FILENAME_SIZE \
	(sizeof("/sys/devices/system/cpu/cpu" STRINGIFY(UINT32_MAX) "/topology/thread_siblings_list"))
#define THREAD_SIBLINGS_FILENAME_FORMAT "/sys/devices/system/cpu/cpu%" PRIu32 "/topology/thread_siblings_list"

#define POSSIBLE_CPULIST_FILENAME "/sys/devices/system/cpu/possible"
#define PRESENT_CPULIST_FILENAME "/sys/devices/system/cpu/present"

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

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

#if defined(__ANDROID__) && !defined(CPU_SETSIZE)
/*
 * Android NDK headers before platform 21 do not define CPU_SETSIZE,
 * so we hard-code its value, as defined in platform 21 headers
 */
#if defined(__LP64__)
static const uint32_t default_max_processors_count = 1024;
#else
static const uint32_t default_max_processors_count = 32;
#endif
#else
static const uint32_t default_max_processors_count = CPU_SETSIZE;
#endif

static bool uint32_parser(const char* filename, const char* text_start, const char* text_end, void* context) {
	if (text_start == text_end) {
		cpuinfo_log_error("failed to parse file %s: file is empty", KERNEL_MAX_FILENAME);
		return false;
	}

	uint32_t kernel_max = 0;
	const char* parsed_end = parse_number(text_start, text_end, &kernel_max);
	if (parsed_end == text_start) {
		cpuinfo_log_error(
			"failed to parse file %s: \"%.*s\" is not an unsigned number",
			filename,
			(int)(text_end - text_start),
			text_start);
		return false;
	} else {
		for (const char* char_ptr = parsed_end; char_ptr != text_end; char_ptr++) {
			if (!is_whitespace(*char_ptr)) {
				cpuinfo_log_warning(
					"non-whitespace characters \"%.*s\" following number in file %s are ignored",
					(int)(text_end - char_ptr),
					char_ptr,
					filename);
				break;
			}
		}
	}

	uint32_t* kernel_max_ptr = (uint32_t*)context;
	*kernel_max_ptr = kernel_max;
	return true;
}

uint32_t cpuinfo_linux_get_max_processors_count(void) {
	uint32_t kernel_max;
	if (cpuinfo_linux_parse_small_file(KERNEL_MAX_FILENAME, KERNEL_MAX_FILESIZE, uint32_parser, &kernel_max)) {
		cpuinfo_log_debug("parsed kernel_max value of %" PRIu32 " from %s", kernel_max, KERNEL_MAX_FILENAME);

		if (kernel_max >= default_max_processors_count) {
			cpuinfo_log_warning(
				"kernel_max value of %" PRIu32
				" parsed from %s exceeds platform-default limit %" PRIu32,
				kernel_max,
				KERNEL_MAX_FILENAME,
				default_max_processors_count - 1);
		}

		return kernel_max + 1;
	} else {
		cpuinfo_log_warning(
			"using platform-default max processors count = %" PRIu32, default_max_processors_count);
		return default_max_processors_count;
	}
}

uint32_t cpuinfo_linux_get_processor_cur_frequency(uint32_t processor) {
	char cur_frequency_filename[FREQUENCY_FILENAME_SIZE];
	const int chars_formatted =
		snprintf(cur_frequency_filename, FREQUENCY_FILENAME_SIZE, CUR_FREQUENCY_FILENAME_FORMAT, processor);
	if ((unsigned int)chars_formatted >= FREQUENCY_FILENAME_SIZE) {
		cpuinfo_log_warning("failed to format filename for current frequency of processor %" PRIu32, processor);
		return 0;
	}

	uint32_t cur_frequency;
	if (cpuinfo_linux_parse_small_file(cur_frequency_filename, FREQUENCY_FILESIZE, uint32_parser, &cur_frequency)) {
		cpuinfo_log_debug(
			"parsed currrent frequency value of %" PRIu32 " KHz for logical processor %" PRIu32 " from %s",
			cur_frequency,
			processor,
			cur_frequency_filename);
		return cur_frequency;
	} else {
		cpuinfo_log_warning(
			"failed to parse current frequency for processor %" PRIu32 " from %s",
			processor,
			cur_frequency_filename);
		return 0;
	}
}

uint32_t cpuinfo_linux_get_processor_max_frequency(uint32_t processor) {
	char max_frequency_filename[FREQUENCY_FILENAME_SIZE];
	const int chars_formatted =
		snprintf(max_frequency_filename, FREQUENCY_FILENAME_SIZE, MAX_FREQUENCY_FILENAME_FORMAT, processor);
	if ((unsigned int)chars_formatted >= FREQUENCY_FILENAME_SIZE) {
		cpuinfo_log_warning("failed to format filename for max frequency of processor %" PRIu32, processor);
		return 0;
	}

	uint32_t max_frequency;
	if (cpuinfo_linux_parse_small_file(max_frequency_filename, FREQUENCY_FILESIZE, uint32_parser, &max_frequency)) {
		cpuinfo_log_debug(
			"parsed max frequency value of %" PRIu32 " KHz for logical processor %" PRIu32 " from %s",
			max_frequency,
			processor,
			max_frequency_filename);
		return max_frequency;
	} else {
		cpuinfo_log_warning(
			"failed to parse max frequency for processor %" PRIu32 " from %s",
			processor,
			max_frequency_filename);
		return 0;
	}
}

uint32_t cpuinfo_linux_get_processor_min_frequency(uint32_t processor) {
	char min_frequency_filename[FREQUENCY_FILENAME_SIZE];
	const int chars_formatted =
		snprintf(min_frequency_filename, FREQUENCY_FILENAME_SIZE, MIN_FREQUENCY_FILENAME_FORMAT, processor);
	if ((unsigned int)chars_formatted >= FREQUENCY_FILENAME_SIZE) {
		cpuinfo_log_warning("failed to format filename for min frequency of processor %" PRIu32, processor);
		return 0;
	}

	uint32_t min_frequency;
	if (cpuinfo_linux_parse_small_file(min_frequency_filename, FREQUENCY_FILESIZE, uint32_parser, &min_frequency)) {
		cpuinfo_log_debug(
			"parsed min frequency value of %" PRIu32 " KHz for logical processor %" PRIu32 " from %s",
			min_frequency,
			processor,
			min_frequency_filename);
		return min_frequency;
	} else {
		/*
		 * This error is less severe than parsing max frequency, because
		 * min frequency is only useful for clustering, while max
		 * frequency is also needed for peak FLOPS calculation.
		 */
		cpuinfo_log_info(
			"failed to parse min frequency for processor %" PRIu32 " from %s",
			processor,
			min_frequency_filename);
		return 0;
	}
}

bool cpuinfo_linux_get_processor_core_id(uint32_t processor, uint32_t core_id_ptr[restrict static 1]) {
	char core_id_filename[PACKAGE_ID_FILENAME_SIZE];
	const int chars_formatted =
		snprintf(core_id_filename, CORE_ID_FILENAME_SIZE, CORE_ID_FILENAME_FORMAT, processor);
	if ((unsigned int)chars_formatted >= CORE_ID_FILENAME_SIZE) {
		cpuinfo_log_warning("failed to format filename for core id of processor %" PRIu32, processor);
		return 0;
	}

	uint32_t core_id;
	if (cpuinfo_linux_parse_small_file(core_id_filename, CORE_ID_FILESIZE, uint32_parser, &core_id)) {
		cpuinfo_log_debug(
			"parsed core id value of %" PRIu32 " for logical processor %" PRIu32 " from %s",
			core_id,
			processor,
			core_id_filename);
		*core_id_ptr = core_id;
		return true;
	} else {
		cpuinfo_log_info(
			"failed to parse core id for processor %" PRIu32 " from %s", processor, core_id_filename);
		return false;
	}
}

bool cpuinfo_linux_get_processor_package_id(uint32_t processor, uint32_t package_id_ptr[restrict static 1]) {
	char package_id_filename[PACKAGE_ID_FILENAME_SIZE];
	const int chars_formatted =
		snprintf(package_id_filename, PACKAGE_ID_FILENAME_SIZE, PACKAGE_ID_FILENAME_FORMAT, processor);
	if ((unsigned int)chars_formatted >= PACKAGE_ID_FILENAME_SIZE) {
		cpuinfo_log_warning("failed to format filename for package id of processor %" PRIu32, processor);
		return 0;
	}

	uint32_t package_id;
	if (cpuinfo_linux_parse_small_file(package_id_filename, PACKAGE_ID_FILESIZE, uint32_parser, &package_id)) {
		cpuinfo_log_debug(
			"parsed package id value of %" PRIu32 " for logical processor %" PRIu32 " from %s",
			package_id,
			processor,
			package_id_filename);
		*package_id_ptr = package_id;
		return true;
	} else {
		cpuinfo_log_info(
			"failed to parse package id for processor %" PRIu32 " from %s", processor, package_id_filename);
		return false;
	}
}

static bool max_processor_number_parser(uint32_t processor_list_start, uint32_t processor_list_end, void* context) {
	uint32_t* processor_number_ptr = (uint32_t*)context;
	const uint32_t processor_list_last = processor_list_end - 1;
	if (*processor_number_ptr < processor_list_last) {
		*processor_number_ptr = processor_list_last;
	}
	return true;
}

uint32_t cpuinfo_linux_get_max_possible_processor(uint32_t max_processors_count) {
	uint32_t max_possible_processor = 0;
	if (!cpuinfo_linux_parse_cpulist(
		    POSSIBLE_CPULIST_FILENAME, max_processor_number_parser, &max_possible_processor)) {
#if CPUINFO_ARCH_ARM || CPUINFO_ARCH_ARM64
		cpuinfo_log_error("failed to parse the list of possible processors in %s", POSSIBLE_CPULIST_FILENAME);
#else
		cpuinfo_log_warning("failed to parse the list of possible processors in %s", POSSIBLE_CPULIST_FILENAME);
#endif
		return UINT32_MAX;
	}
	if (max_possible_processor >= max_processors_count) {
		cpuinfo_log_warning(
			"maximum possible processor number %" PRIu32 " exceeds system limit %" PRIu32
			": truncating to the latter",
			max_possible_processor,
			max_processors_count - 1);
		max_possible_processor = max_processors_count - 1;
	}
	return max_possible_processor;
}

uint32_t cpuinfo_linux_get_max_present_processor(uint32_t max_processors_count) {
	uint32_t max_present_processor = 0;
	if (!cpuinfo_linux_parse_cpulist(
		    PRESENT_CPULIST_FILENAME, max_processor_number_parser, &max_present_processor)) {
#if CPUINFO_ARCH_ARM || CPUINFO_ARCH_ARM64
		cpuinfo_log_error("failed to parse the list of present processors in %s", PRESENT_CPULIST_FILENAME);
#else
		cpuinfo_log_warning("failed to parse the list of present processors in %s", PRESENT_CPULIST_FILENAME);
#endif
		return UINT32_MAX;
	}
	if (max_present_processor >= max_processors_count) {
		cpuinfo_log_warning(
			"maximum present processor number %" PRIu32 " exceeds system limit %" PRIu32
			": truncating to the latter",
			max_present_processor,
			max_processors_count - 1);
		max_present_processor = max_processors_count - 1;
	}
	return max_present_processor;
}

struct detect_processors_context {
	uint32_t max_processors_count;
	uint32_t* processor0_flags;
	uint32_t processor_struct_size;
	uint32_t detected_flag;
};

static bool detect_processor_parser(uint32_t processor_list_start, uint32_t processor_list_end, void* context) {
	const uint32_t max_processors_count = ((struct detect_processors_context*)context)->max_processors_count;
	const uint32_t* processor0_flags = ((struct detect_processors_context*)context)->processor0_flags;
	const uint32_t processor_struct_size = ((struct detect_processors_context*)context)->processor_struct_size;
	const uint32_t detected_flag = ((struct detect_processors_context*)context)->detected_flag;

	for (uint32_t processor = processor_list_start; processor < processor_list_end; processor++) {
		if (processor >= max_processors_count) {
			break;
		}
		*((uint32_t*)((uintptr_t)processor0_flags + processor_struct_size * processor)) |= detected_flag;
	}
	return true;
}

bool cpuinfo_linux_detect_possible_processors(
	uint32_t max_processors_count,
	uint32_t* processor0_flags,
	uint32_t processor_struct_size,
	uint32_t possible_flag) {
	struct detect_processors_context context = {
		.max_processors_count = max_processors_count,
		.processor0_flags = processor0_flags,
		.processor_struct_size = processor_struct_size,
		.detected_flag = possible_flag,
	};
	if (cpuinfo_linux_parse_cpulist(POSSIBLE_CPULIST_FILENAME, detect_processor_parser, &context)) {
		return true;
	} else {
		cpuinfo_log_warning("failed to parse the list of possible processors in %s", POSSIBLE_CPULIST_FILENAME);
		return false;
	}
}

bool cpuinfo_linux_detect_present_processors(
	uint32_t max_processors_count,
	uint32_t* processor0_flags,
	uint32_t processor_struct_size,
	uint32_t present_flag) {
	struct detect_processors_context context = {
		.max_processors_count = max_processors_count,
		.processor0_flags = processor0_flags,
		.processor_struct_size = processor_struct_size,
		.detected_flag = present_flag,
	};
	if (cpuinfo_linux_parse_cpulist(PRESENT_CPULIST_FILENAME, detect_processor_parser, &context)) {
		return true;
	} else {
		cpuinfo_log_warning("failed to parse the list of present processors in %s", PRESENT_CPULIST_FILENAME);
		return false;
	}
}

struct siblings_context {
	const char* group_name;
	uint32_t max_processors_count;
	uint32_t processor;
	cpuinfo_siblings_callback callback;
	void* callback_context;
};

static bool siblings_parser(uint32_t sibling_list_start, uint32_t sibling_list_end, struct siblings_context* context) {
	const char* group_name = context->group_name;
	const uint32_t max_processors_count = context->max_processors_count;
	const uint32_t processor = context->processor;

	if (sibling_list_end > max_processors_count) {
		cpuinfo_log_warning(
			"ignore %s siblings %" PRIu32 "-%" PRIu32 " of processor %" PRIu32,
			group_name,
			max_processors_count,
			sibling_list_end - 1,
			processor);
		sibling_list_end = max_processors_count;
	}

	return context->callback(processor, sibling_list_start, sibling_list_end, context->callback_context);
}

bool cpuinfo_linux_detect_core_cpus(
	uint32_t max_processors_count,
	uint32_t processor,
	cpuinfo_siblings_callback callback,
	void* context) {
	char core_cpus_filename[CORE_CPUS_FILENAME_SIZE];
	const int chars_formatted =
		snprintf(core_cpus_filename, CORE_CPUS_FILENAME_SIZE, CORE_CPUS_FILENAME_FORMAT, processor);
	if ((unsigned int)chars_formatted >= CORE_CPUS_FILENAME_SIZE) {
		cpuinfo_log_warning("failed to format filename for core cpus of processor %" PRIu32, processor);
		return false;
	}

	struct siblings_context siblings_context = {
		.group_name = "cpus",
		.max_processors_count = max_processors_count,
		.processor = processor,
		.callback = callback,
		.callback_context = context,
	};
	if (cpuinfo_linux_parse_cpulist(
		    core_cpus_filename, (cpuinfo_cpulist_callback)siblings_parser, &siblings_context)) {
		return true;
	} else {
		cpuinfo_log_info(
			"failed to parse the list of core cpus for processor %" PRIu32 " from %s",
			processor,
			core_cpus_filename);
		return false;
	}
}

bool cpuinfo_linux_detect_core_siblings(
	uint32_t max_processors_count,
	uint32_t processor,
	cpuinfo_siblings_callback callback,
	void* context) {
	char core_siblings_filename[CORE_SIBLINGS_FILENAME_SIZE];
	const int chars_formatted =
		snprintf(core_siblings_filename, CORE_SIBLINGS_FILENAME_SIZE, CORE_SIBLINGS_FILENAME_FORMAT, processor);
	if ((unsigned int)chars_formatted >= CORE_SIBLINGS_FILENAME_SIZE) {
		cpuinfo_log_warning("failed to format filename for core siblings of processor %" PRIu32, processor);
		return false;
	}

	struct siblings_context siblings_context = {
		.group_name = "package",
		.max_processors_count = max_processors_count,
		.processor = processor,
		.callback = callback,
		.callback_context = context,
	};
	if (cpuinfo_linux_parse_cpulist(
		    core_siblings_filename, (cpuinfo_cpulist_callback)siblings_parser, &siblings_context)) {
		return true;
	} else {
		cpuinfo_log_info(
			"failed to parse the list of core siblings for processor %" PRIu32 " from %s",
			processor,
			core_siblings_filename);
		return false;
	}
}

bool cpuinfo_linux_detect_thread_siblings(
	uint32_t max_processors_count,
	uint32_t processor,
	cpuinfo_siblings_callback callback,
	void* context) {
	char thread_siblings_filename[THREAD_SIBLINGS_FILENAME_SIZE];
	const int chars_formatted = snprintf(
		thread_siblings_filename, THREAD_SIBLINGS_FILENAME_SIZE, THREAD_SIBLINGS_FILENAME_FORMAT, processor);
	if ((unsigned int)chars_formatted >= THREAD_SIBLINGS_FILENAME_SIZE) {
		cpuinfo_log_warning("failed to format filename for thread siblings of processor %" PRIu32, processor);
		return false;
	}

	struct siblings_context siblings_context = {
		.group_name = "core",
		.max_processors_count = max_processors_count,
		.processor = processor,
		.callback = callback,
		.callback_context = context,
	};
	if (cpuinfo_linux_parse_cpulist(
		    thread_siblings_filename, (cpuinfo_cpulist_callback)siblings_parser, &siblings_context)) {
		return true;
	} else {
		cpuinfo_log_info(
			"failed to parse the list of thread siblings for processor %" PRIu32 " from %s",
			processor,
			thread_siblings_filename);
		return false;
	}
}

bool cpuinfo_linux_detect_cluster_cpus(
	uint32_t max_processors_count,
	uint32_t processor,
	cpuinfo_siblings_callback callback,
	void* context) {
	char cluster_cpus_filename[CLUSTER_CPUS_FILENAME_SIZE];
	const int chars_formatted =
		snprintf(cluster_cpus_filename, CLUSTER_CPUS_FILENAME_SIZE, CLUSTER_CPUS_FILENAME_FORMAT, processor);
	if ((unsigned int)chars_formatted >= CLUSTER_CPUS_FILENAME_SIZE) {
		cpuinfo_log_warning("failed to format filename for cluster cpus of processor %" PRIu32, processor);
		return false;
	}

	struct siblings_context siblings_context = {
		.group_name = "cluster",
		.max_processors_count = max_processors_count,
		.processor = processor,
		.callback = callback,
		.callback_context = context,
	};
	if (cpuinfo_linux_parse_cpulist(
		    cluster_cpus_filename, (cpuinfo_cpulist_callback)siblings_parser, &siblings_context)) {
		return true;
	} else {
		cpuinfo_log_info(
			"failed to parse the list of cluster cpus for processor %" PRIu32 " from %s",
			processor,
			cluster_cpus_filename);
		return false;
	}
}

bool cpuinfo_linux_detect_package_cpus(
	uint32_t max_processors_count,
	uint32_t processor,
	cpuinfo_siblings_callback callback,
	void* context) {
	char package_cpus_filename[PACKAGE_CPUS_FILENAME_SIZE];
	const int chars_formatted =
		snprintf(package_cpus_filename, PACKAGE_CPUS_FILENAME_SIZE, PACKAGE_CPUS_FILENAME_FORMAT, processor);
	if ((unsigned int)chars_formatted >= PACKAGE_CPUS_FILENAME_SIZE) {
		cpuinfo_log_warning("failed to format filename for package cpus of processor %" PRIu32, processor);
		return false;
	}

	struct siblings_context siblings_context = {
		.group_name = "package",
		.max_processors_count = max_processors_count,
		.processor = processor,
		.callback = callback,
		.callback_context = context,
	};
	if (cpuinfo_linux_parse_cpulist(
		    package_cpus_filename, (cpuinfo_cpulist_callback)siblings_parser, &siblings_context)) {
		return true;
	} else {
		cpuinfo_log_info(
			"failed to parse the list of package cpus for processor %" PRIu32 " from %s",
			processor,
			package_cpus_filename);
		return false;
	}
}
