/* DMI (Desktop Management Interface)
   also called
   SMBIOS (System Management BIOS)
*/

#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "common.h"

char *dmidecode_file = NULL;


static const char *cat_BIOS[] = { "Vendor", "Version" };
static const char *cat_System[] = { "Manufacturer", "Product Name", "Version" };
static const char *cat_Base_Board[] = { "Manufacturer", "Product Name", "Version" };

struct category {
	const char *cat_name;
	unsigned int nb_fields;
	const char **fields;
};

static const struct category categories[] = {
	{ "BIOS", psizeof(cat_BIOS), cat_BIOS },
	{ "System", psizeof(cat_System), cat_System },
	{ "Base Board", psizeof(cat_Base_Board), cat_Base_Board },

};
static int nb_categories = psizeof(categories);

struct criterion {
	char *name;
	char *val;
};
struct criteria {
	unsigned int nb;
	struct criterion *criteria;
};

/*********************************************************************************/
static int streq(const char *s1, const char *s2) {
	return strcmp(s1, s2) == 0;
}
static int str_begins_with(const char *s, const char *prefix) {
	return strncmp(s, prefix, strlen(prefix)) == 0;
}
static int remove_suffix_in_place(char *s, const char *suffix) {
	unsigned int l = strlen(s);
	unsigned int l_suffix = strlen(suffix);
	if (l >= l_suffix && streq(s + l - l_suffix, suffix)) {
		s[l - l_suffix] = '\0';
		return 1;
	} else
		return 0;
}
static void remove_ending_spaces(char *s) {
	char *p;
	for (p = s + strlen(s) - 1; p >= s; p--) {
		if (*p != '\n' && *p != '\r' && *p != ' ' && *p != '\t')
			break;
		*p = '\0';
	}
}
static char *skip_leading_spaces(char *s) {
	for (; *s; s++)
		if (*s != '\n' && *s != '\r' && *s != ' ' && *s != '\t')
			break;
	return s;
}

/*********************************************************************************/
static char *get_after_colon(char *s) {
	char *p = strchr(s, ':');
	if (p) {
		*p = '\0';
		return p + (p[1] == ' ' ? 2 : 1);
	} else return NULL;
}

static const struct category *lookup_category(const char *cat_name) {
	int i;
	for (i = 0; i < nb_categories; i++)
		if (streq(categories[i].cat_name, cat_name))
			return &categories[i];
	return NULL;
}

static int lookup_field(const struct category *category, const char *field_name) {
	unsigned int i;
	for (i = 0; i < category->nb_fields; i++)
		if (streq(category->fields[i], field_name))
			return 1;
	return 0;
}

static char *lookup_criteria(struct criteria criteria, const char *field) {
	unsigned int i;
	for (i = 0; i < criteria.nb; i++)
		if (streq(criteria.criteria[i].name, field))
			return criteria.criteria[i].val;
	return NULL;
}

/*********************************************************************************/
static struct criteria criteria_from_dmidecode(void) {
	FILE *f;
	char buf[BUF_SIZE];

	struct criteria r = {0, NULL};

	if (!(f = dmidecode_file ? fopen(dmidecode_file, "r") : popen("dmidecode", "r"))) {
		perror("dmidecode");
		return r;
	}

	r.criteria = malloc(sizeof(*r.criteria) * MAX_DEVICES);

	const struct category *category = NULL;

	/* dmidecode output is less indented as of 2.7 */
	int tab_level = 1;
        if (fgets(buf, sizeof(buf) - 1, f)) {
		int major, minor;
		if (sscanf(buf, "# dmidecode %d.%d", &major, &minor) == 2 && major >= 2 && minor >= 7)
			tab_level = 0;
	}

	while (fgets(buf, sizeof(buf) - 1, f)) {
		if (!buf[0] || !buf[1] || (tab_level && buf[0] != '\t'))
			; /* don't care */
		else if (buf[tab_level] != '\t') {
			char *s = buf + tab_level;
			if (!str_begins_with(s, "DMI type ")) {
				remove_ending_spaces(s);
				remove_suffix_in_place(s, " Information");
				category = lookup_category(s);
			}
		} else if (category) {
			/* don't even look if we don't have an interesting category */
			char *s = buf + tab_level + 1;
			char *val = get_after_colon(s);
			if (val && lookup_field(category, s)) {
				struct criterion *criterion = &r.criteria[r.nb++];
				asprintf(&criterion->name, "%s/%s", category->cat_name, s);
				remove_ending_spaces(val);
				criterion->val = strdup(skip_leading_spaces(val));
			}
		}
	}
	if (dmidecode_file ? fclose(f) != 0 : pclose(f) == -1) {
		r.nb = 0;
		return r;
	}
	r.criteria = realloc(r.criteria, sizeof(*r.criteria) * r.nb);

	return r;
}

static void free_criteria(struct criteria criteria) {
	unsigned int i;
	for (i = 0; i < criteria.nb; i++) {
		free(criteria.criteria[i].name);
		free(criteria.criteria[i].val);
	}
	if (criteria.nb) free(criteria.criteria);
	criteria.nb = 0;
}

static struct dmi_entries entries_matching_criteria(struct criteria criteria) {
	fh f;
	char buf[2048];
	int line;
	struct dmi_entries r;
	#define MAX_INDENT 20
	int valid[MAX_INDENT];
	char *constraints[MAX_INDENT];

	enum state { in_constraints, in_implies } state = in_implies;
	int was_a_blank_line = 1;

	r.nb = 0;
	f = fh_open("dmitable");

#define die(err) do { fprintf(stderr, "%s %d: " err "\n", "dmitable", line); exit(1); } while (0)

	r.entries = malloc(sizeof(*r.entries) * MAX_DEVICES);

#define foreach_indent(min, action) do { int i; for (i = min; i < MAX_INDENT; i++) { action; } } while (0)
	
	foreach_indent(0, valid[i] = 1; constraints[i] = NULL);

	int previous_refine = 0;

	for (line = 1; fh_gets(buf, sizeof(buf) - 1, &f); line++) {
		char *s = skip_leading_spaces(buf);
		if (*s == '#') continue; // skip comments

		if (!*s) {
			was_a_blank_line = 1;
		} else {
			int refine = s - buf;
			if (refine > MAX_INDENT) die("too indented constraints");

			remove_ending_spaces(s);
			if (str_begins_with(s, "=> ")) {
				if (refine != previous_refine) die("\"=>\" must not be indented");
				state = in_implies;
				was_a_blank_line = 0;
				
				if (valid[refine]) {
					struct dmi_entry *entry = &r.entries[r.nb++];

					s += strlen("=> ");
					char *val = get_after_colon(s);
					if (!val) die("invalid value");
					asprintf(&entry->module, "%s:%s", s, val);

					char tmp[BUF_SIZE];
					tmp[0] = '\0';

					int i;
					for (i = 0; i <= refine; i++)
						if (constraints[i]) {
							if (i) strncat(tmp, "|", BUF_SIZE);
							strncat(tmp, constraints[i], BUF_SIZE);
						}
					entry->constraints = strdup(tmp);
				}
			} else {
				if (state == in_constraints && refine == previous_refine) die("to refine, indent");
				if (!was_a_blank_line) die("missing empty line");
				state = in_constraints;
				previous_refine = refine;
				was_a_blank_line = 0;

				if (refine == 0 || valid[refine - 1]) {

					char *wanted_val = get_after_colon(s);
					if (!wanted_val) die("bad format");

					char *wanted_val_orig = strdup(wanted_val);
					char *val = lookup_criteria(criteria, s);

					int ok = wanted_val && val && (
						remove_suffix_in_place(wanted_val, ".*") ?
						str_begins_with(val, wanted_val) :
						streq(val, wanted_val));
				
					foreach_indent(refine, valid[i] = ok; ifree(constraints[i]));
					if (ok) 
						constraints[refine] = wanted_val_orig;
					else
						free(wanted_val_orig);

				} /* otherwise no need checking */				
			}			
		}
	}
	foreach_indent(0, ifree(constraints[i]));
	fh_close(&f);

	r.entries = realloc(r.entries, sizeof(*r.entries) * r.nb);
	return r;
}

extern void dmi_entries_free(struct dmi_entries entries) {
	unsigned int i;
	for (i = 0; i < entries.nb; i++) {
		free(entries.entries[i].constraints);
		free(entries.entries[i].module);
	}
	if (entries.nb) free(entries.entries);
	entries.nb = 0;
}

extern struct dmi_entries dmi_probe(void) {
	struct criteria criteria = criteria_from_dmidecode();
	struct dmi_entries entries = entries_matching_criteria(criteria);
	free_criteria(criteria);

	return entries;
}