/*
 * zdev - Modify and display the persistent configuration of devices
 *
 * Copyright IBM Corp. 2016, 2019
 *
 * s390-tools is free software; you can redistribute it and/or modify
 * it under the terms of the MIT license. See LICENSE for details.
 */

#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/utsname.h>

#include "attrib.h"
#include "device.h"
#include "devtype.h"
#include "export.h"
#include "sanitize.h"
#include "setting.h"
#include "subtype.h"

static int import_site_id = SITE_FALLBACK;

struct export_header {
	export_t type;
	config_t config;
	char *type_id;
	char *id;
};

static bool header_str_to_config(const char *config_str, config_t *config)
{
	int site;

	if (strstr(config_str, "site")) {
		if (sscanf(config_str, "site%d", &site) == 1) {
			if (site < 0 || site >= NUM_USER_SITES)
				return false;
			import_site_id = site;
			*config = config_persistent;
			return true;
		}
	}
	import_site_id = SITE_FALLBACK;
	return str_to_config(config_str, config);
}

static struct export_header *header_new(const char *config_str,
					const char *type, const char *id)
{
	config_t config;
	struct export_header *hdr;

	if (!header_str_to_config(config_str, &config))
		return NULL;
	hdr = misc_malloc(sizeof(struct export_header));
	if (!id)
		hdr->type = export_devtype;
	else
		hdr->type = export_device;
	hdr->config = config;
	hdr->type_id = misc_strdup(type);
	if (id)
		hdr->id = misc_strdup(id);

	return hdr;
}

static void header_free(struct export_header *hdr)
{
	free(hdr->type_id);
	free(hdr->id);
	free(hdr);
}

static void write_key_val(FILE *fd, const char *name, const char *value)
{
	char *str;

	str = quote_str(value, 0);
	fprintf(fd, "%s=%s\n", name, str);
	free(str);
}

static bool is_exportable(struct setting *s, config_t config)
{
	struct attrib *a = s->attrib;

	if (!a)
		return true;
	if (a->readonly)
		return false;
	if (a->mandatory)
		return true;
	if (SCOPE_ACTIVE(config)) {
		if (a->unstable && !a->map) {
			/* Skip values that cannot be determined. */
			return false;
		}
		if (!attrib_check_value(a, s->value)) {
			/* Skip values that are not acceptable input values. */
			return false;
		}
		if (!attrib_match_default(s->attrib, s->value)) {
			/* All non-default values should be exported. */
			return true;
		}
	}
	if (SCOPE_PERSISTENT(config) || SCOPE_AUTOCONF(config)) {
		if (setting_is_set(s))
			return true;
	}

	return false;
}

static void write_settings(FILE *fd, struct setting_list *settings,
			   config_t config)
{
	struct setting *s;
	struct strlist_node *v;

	if (!settings)
		return;
	util_list_iterate(&settings->list, s) {
		if (!is_exportable(s, config))
			continue;
		if (s->values) {
			/* Multiple values. */
			util_list_iterate(s->values, v)
				write_key_val(fd, s->name, v->str);
		} else {
			/* Single value. */
			write_key_val(fd, s->name, s->value);
		}
	}
}

/* Write an initial comment. */
static void export_write_comment(FILE *fd)
{
	struct utsname uts;

	if (uname(&uts) == 0)
		fprintf(fd, "# Generated by chzdev on %s\n", uts.nodename);
	else
		fprintf(fd, "# Generated by chzdev\n");
}

static const char *site_specific_header(config_t config)
{
	if (global_site_id != SITE_FALLBACK)
		return misc_asprintf("site%d", global_site_id);
	else
		return config_to_str(config);
}

/* Write an export file header. */
static void write_header(FILE *fd, config_t config, const char *type,
			 const char *id, int *first_ptr)
{
	if (first_ptr) {
		if (*first_ptr) {
			export_write_comment(fd);
			*first_ptr = 0;
		} else
			fprintf(fd, "\n");
	}

	if (id)
		fprintf(fd, "[%s %s %s]\n", site_specific_header(config), type, id);
	else
		fprintf(fd, "[%s %s]\n", site_specific_header(config), type);
}

static int count_exportable(struct device *dev, config_t config)
{
	struct setting_list *list;
	struct setting *s;
	int count;

	list = device_get_setting_list(dev, config, global_site_id);
	if (!list)
		return 0;
	count = 0;

	util_list_iterate(&list->list, s) {
		if (is_exportable(s, config))
			count++;
	}

	return count;
}

/* Write settings for device @dev in the specified @config to @fd. */
exit_code_t export_write_device(FILE *fd, struct device *dev, config_t config,
				int *first_ptr)
{
	exit_code_t rc;
	struct setting_list *settings;

	if (!SCOPE_SINGLE(config)) {
		if (SCOPE_ACTIVE(config)) {
			rc = export_write_device(fd, dev, config_active,
						 first_ptr);
			if (rc)
				return rc;
		}
		if (SCOPE_PERSISTENT(config)) {
			rc = export_write_device(fd, dev, config_persistent,
						 first_ptr);
			if (rc)
				return rc;
		}
		if (SCOPE_AUTOCONF(config)) {
			rc = export_write_device(fd, dev, config_autoconf,
						 first_ptr);
			if (rc)
				return rc;
		}
		return EXIT_OK;
	}

	if (config == config_active) {
		if (!dev->active.exists)
			return EXIT_OK;
		/* No need to export device with no settings. */
		if (count_exportable(dev, config_active) == 0 &&
		    !dev->subtype->support_definable)
			return EXIT_OK;
		settings = dev->active.settings;
	} else if (config == config_persistent) {
		if (!dev->persistent.exists)
			return EXIT_OK;
		settings = dev->persistent.settings;
	} else {
		if (!dev->autoconf.exists)
			return EXIT_OK;
		settings = dev->autoconf.settings;
	}

	write_header(fd, config, dev->subtype->name, dev->id, first_ptr);
	write_settings(fd, settings, config);

	return ferror(fd) ? EXIT_RUNTIME_ERROR : EXIT_OK;
}

/* Write settings for device type @dt in the specified @config to @fd. */
exit_code_t export_write_devtype(FILE *fd, struct devtype *dt, config_t config,
				 int *first_ptr)
{
	exit_code_t rc;
	struct setting_list *settings;

	if (!SCOPE_SINGLE(config)) {
		if (SCOPE_ACTIVE(config)) {
			rc = export_write_devtype(fd, dt, config_active,
						  first_ptr);
			if (rc)
				return rc;
		}
		if (SCOPE_PERSISTENT(config)) {
			rc = export_write_devtype(fd, dt, config_persistent,
						  first_ptr);
			if (rc)
				return rc;
		}
		/* Scope autoconf not supported for devtype */
		return EXIT_OK;
	}

	if (config == config_active)
		settings = dt->active_settings;
	else
		settings = dt->persistent_settings;

	if (!settings || setting_list_count_set(settings) == 0)
		return EXIT_OK;

	write_header(fd, config, dt->name, NULL, first_ptr);
	write_settings(fd, settings, config);

	return ferror(fd) ? EXIT_RUNTIME_ERROR : EXIT_OK;
}

static bool parse_header(const char *line, struct export_header **header_ptr)
{
	char *copy;
	size_t end;
	int argc;
	char **argv;
	struct export_header *header;

	if (*line != '[')
		return false;
	end = strlen(line) - 1;
	if (line[end] != ']')
		return false;

	copy = misc_strdup(line);
	copy[end] = 0;
	line_split(&copy[1], &argc, &argv);
	free(copy);

	if (argc == 2)
		header = header_new(argv[0], argv[1], NULL);
	else if (argc == 3)
		header = header_new(argv[0], argv[1], argv[2]);
	else
		header = NULL;

	line_free(argc, argv);

	if (!header)
		return false;

	*header_ptr = header;

	return true;
}

static bool parse_setting(const char *line, char **key_ptr, char **val_ptr)
{
	char *copy, *value;

	copy = misc_strdup(line);
	value = strchr(copy, '=');
	if (!value) {
		free(copy);
		return false;
	}

	*value = 0;
	value++;
	*key_ptr = shrink_str(copy);
	*val_ptr = unquote_str(value);
	sanitize(*key_ptr, VALID_ATTRKEY);
	sanitize(*val_ptr, VALID_ATTRVAL);
	free(copy);

	return true;
}

static struct setting_list *dt_get_setting_list(struct devtype *dt,
						config_t config)
{
	struct setting_list *settings = NULL;

	if (config == config_active)
		settings = dt->active_settings;
	else
		settings = dt->persistent_settings;

	return settings;
}

/* Display a warning related to a line in an export data file. */
static void fwarn(const char *filename, int lineno, const char *format, ...)
{
	va_list args;

	va_start(args, format);
	fprintf(stderr, "%s:%d: ", filename, lineno);
	vfprintf(stderr, format, args);
	va_end(args);
}

static exit_code_t handle_header(const char *filename, int lineno,
				 struct export_header *hdr,
				 struct devtype **dt_ptr,
				 struct device **dev_ptr)
{
	struct devtype *dt = NULL;
	struct device *dev = NULL;
	struct subtype *st;
	exit_code_t rc = EXIT_OK;

	if (hdr->type == export_devtype) {
		/* Section header indicates device type settings. */
		dt = devtype_find(hdr->type_id);
		if (!dt) {
			fwarn(filename, lineno, "Unknown device type '%s' in "
			      "section header\n", hdr->type_id);
			rc = EXIT_FORMAT_ERROR;
			goto out;
		}
		/* Prepare device type for new settings. */
		if (SCOPE_ACTIVE(hdr->config)) {
			setting_list_free(dt->active_settings);
			dt->active_settings = setting_list_new();
		}
		if (SCOPE_PERSISTENT(hdr->config)) {
			setting_list_free(dt->persistent_settings);
			dt->persistent_settings = setting_list_new();
		}
	} else if (hdr->type == export_device) {
		/* Section header indicates device settings. */
		st = subtype_find(hdr->type_id);
		if (!st) {
			if (devtype_find(hdr->type_id)) {
				fwarn(filename, lineno, "Ambiguous device "
				      "type '%s' in section header\n",
				      hdr->type_id);
			} else {
				fwarn(filename, lineno, "Unknown device type "
				      "'%s' in section header\n", hdr->type_id);
			}
			rc = EXIT_FORMAT_ERROR;
			goto out;
		}

		if (!st->devices)
			st->devices = device_list_new(st);

		dev = device_list_find(st->devices, hdr->id, NULL);
		if (!dev) {
			/* Register a new device. */
			dev = device_new(st, hdr->id);
			if (!dev) {
				fwarn(filename, lineno, "Unknown device ID "
				      "format '%s' in section header\n",
				      hdr->id);
				rc = EXIT_FORMAT_ERROR;
				goto out;
			}
			device_list_add(st->devices, dev);
		}

		/* Prepare device for new settings. */
		if (SCOPE_ACTIVE(hdr->config)) {
			setting_list_clear(dev->active.settings);
			if (dev->subtype->support_definable)
				dev->active.definable = 1;
			else
				dev->active.exists = 1;
		}
		if (SCOPE_PERSISTENT(hdr->config)) {
			if (import_site_id != SITE_FALLBACK)
				setting_list_clear(dev->site_specific[import_site_id].settings);
			else
				setting_list_clear(dev->persistent.settings);

			dev->persistent.exists = 1;
		}
		if (SCOPE_AUTOCONF(hdr->config)) {
			setting_list_clear(dev->autoconf.settings);
			dev->autoconf.exists = 1;
		}
	}

out:
	*dt_ptr = dt;
	*dev_ptr = dev;

	return rc;
}

static exit_code_t handle_setting(const char *filename, int lineno,
				  const char *key, const char *value,
				  struct devtype *dt, struct device *dev,
				  config_t config)
{
	exit_code_t rc;
	struct attrib **attribs, *a;
	struct setting_list *list;

	if (!SCOPE_SINGLE(config)) {
		if (SCOPE_ACTIVE(config)) {
			rc = handle_setting(filename, lineno, key, value, dt,
					    dev, config_active);
			if (rc)
				return rc;
		}
		if (SCOPE_PERSISTENT(config)) {
			rc = handle_setting(filename, lineno, key, value, dt,
					    dev, config_persistent);
			if (rc)
				return rc;
		}
		if (SCOPE_AUTOCONF(config)) {
			rc = handle_setting(filename, lineno, key, value, dt,
					    dev, config_autoconf);
			if (rc)
				return rc;
		}
		return EXIT_OK;
	}

	if (dt) {
		/* We're inside a device type section. */
		attribs = dt->type_attribs;
		list = dt_get_setting_list(dt, config);
	} else if (dev) {
		/* We're inside a device section. */
		attribs = dev->subtype->dev_attribs;
		list = device_get_setting_list(dev, config, SITE_FALLBACK);
	} else
		return EXIT_OK;

	a = attrib_find(attribs, key);
	if (a)
		setting_list_apply(list, a, a->name, value);
	else if (dt) {
		fwarn(filename, lineno, "Skipping unknown device type "
		      "setting %s=%s\n", key, value);
	} else {
		fwarn(filename, lineno, "Skipping unknown device setting "
		      "%s=%s\n", key, value);
	}

	return EXIT_OK;
}

struct export_object *object_new(export_t type, void *ptr)
{
	struct export_object *obj;

	obj = misc_malloc(sizeof(struct export_object));
	obj->type = type;
	if (type == export_devtype)
		obj->ptr.dt = ptr;
	else
		obj->ptr.dev = ptr;

	return obj;
}

static bool empty_device(struct device *dev)
{
	/* Empty device settings are ok for subtypes supporting definable
	 * devices because there the device ID is information enough. */
	if (dev->subtype->support_definable)
		return false;
	if (util_list_is_empty(&dev->active.settings->list) &&
	    util_list_is_empty(&dev->persistent.settings->list) &&
	    util_list_is_empty(&dev->autoconf.settings->list) &&
	    import_site_id == SITE_FALLBACK)
		return true;
	return false;
}

static bool empty_devtype(struct devtype *dt)
{
	if ((!dt->active_settings ||
	     util_list_is_empty(&dt->active_settings->list)) &&
	    (!dt->persistent_settings ||
	     util_list_is_empty(&dt->persistent_settings->list)))
		return true;
	return false;
}

static bool check_section(const char *filename, int lineno,
			  struct devtype *dt, struct device *dev)
{

	if (dt && empty_devtype(dt)) {
		fwarn(filename, lineno, "Empty device type section\n");
		return false;
	}
	if (dev && empty_device(dev)) {
		fwarn(filename, lineno, "Empty device section\n");
		return false;
	}
	return true;
}

/* Read configuration objects from @fd. Add pointer to newly allocated
 * struct export_objects to ptrlist @objects. */
exit_code_t export_read(FILE *fd, const char *filename,
			struct util_list *objects)
{
	char *line, *l, *key, *value;
	struct export_header *hdr;
	size_t end;
	exit_code_t rc = EXIT_OK;
	struct devtype *dt;
	int lineno;
	config_t config = config_all;
	struct device *dev;

	lineno = 0;
	line = NULL;
	dt = NULL;
	dev = NULL;
	while (rc == EXIT_OK && getline(&line, &end, fd) > 0) {
		lineno++;

		l = shrink_str(line);
		if (!*l || *l == '#') {
			free(l);
			continue;
		}

		if (parse_header(l, &hdr)) {
			if (!check_section(filename, lineno, dt, dev)) {
				header_free(hdr);
				rc = EXIT_FORMAT_ERROR;
				goto err;
			}
			/* Found [<config> <subtype> <devid>] or
			 * [<config> <devtype>]. */
			rc = handle_header(filename, lineno, hdr, &dt, &dev);
			config = hdr->config;
			header_free(hdr);
			if (dt) {
				ptrlist_add(objects, object_new(export_devtype,
								dt));
			} else if (dev) {
				if (global_site_id == import_site_id)
					ptrlist_add(objects, object_new(export_device,
									dev));
			}
		} else if (parse_setting(l, &key, &value)) {
			/* Found <key>=<value>. */
			if (!dt && !dev) {
				fwarn(filename, lineno, "Setting outside of "
				      "valid section\n");
				rc = EXIT_FORMAT_ERROR;
			} else {
				if (global_site_id == import_site_id ||
				    import_site_id == SITE_FALLBACK)
					rc = handle_setting(filename, lineno, key,
							    value, dt, dev, config);
			}
			free(key);
			free(value);
		} else {
			warn("%s:%d: Unrecognized line: %s\n", filename,
			     lineno, l);
			rc = EXIT_FORMAT_ERROR;
		}
err:
		free(l);
	}
	free(line);

	if (rc == EXIT_OK) {
		if (!check_section(filename, lineno, dt, dev))
			rc = EXIT_FORMAT_ERROR;
	}

	if (rc)
		return rc;

	return ferror(fd) ? EXIT_RUNTIME_ERROR : EXIT_OK;
}
