// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Copyright (C) 2012 Jan Luebbe <j.luebbe@pengutronix.de>
 * Copyright (C) 2015 Marc Kleine-Budde <mkl@pengutronix.de>
 */
#define pr_fmt(fmt)	"bootchooser: " fmt

#include <bootchooser.h>
#include <environment.h>
#include <globalvar.h>
#include <magicvar.h>
#include <command.h>
#include <libfile.h>
#include <common.h>
#include <malloc.h>
#include <printk.h>
#include <xfuncs.h>
#include <envfs.h>
#include <errno.h>
#include <fcntl.h>
#include <ioctl.h>
#include <libbb.h>
#include <state.h>
#include <stdio.h>
#include <init.h>
#include <crc.h>
#include <net.h>
#include <fs.h>
#include <reset_source.h>

#include <linux/kernel.h>
#include <linux/list.h>
#include <linux/err.h>

#define BOOTCHOOSER_PREFIX "global.bootchooser"

static char *available_targets;
static char *state_prefix;
static int global_default_attempts = 3;
static int global_default_priority = 1;
static int disable_on_zero_attempts;
static int retry;
static int last_boot_successful;

struct bootchooser {
	struct list_head targets;
	struct bootchooser_target *last_chosen;

	struct state *state;
	char *state_prefix;
	int refs;

	int verbose;
	int dryrun;
};

struct bootchooser_target {
	struct bootchooser *bootchooser;
	struct list_head list;

	/* state */
	unsigned int priority;
	unsigned int remaining_attempts;
	int id;

	/* spec */
	char *name;
	unsigned int default_attempts;
	unsigned int default_priority;

	char *boot;

	char *prefix;
	char *state_prefix;
};

enum reset_attempts {
	RESET_ATTEMPTS_POWER_ON,
	RESET_ATTEMPTS_ALL_ZERO,
};

static unsigned long reset_attempts;

enum reset_priorities {
	RESET_PRIORITIES_ALL_ZERO,
};

static unsigned long reset_priorities;

static int bootchooser_target_ok(struct bootchooser_target *target, const char **reason)
{
	if (!target->priority) {
		if (reason)
			*reason = "Target disabled (priority = 0)";
		return false;
	}

	if (!target->remaining_attempts) {
		if (reason)
			*reason = "remaining attempts = 0";
		return false;
	}

	if (reason)
		*reason = "target OK";

	return true;
}

static void pr_target(struct bootchooser_target *target)
{
	const char *reason;
	int ok;

	ok = bootchooser_target_ok(target, &reason);

	printf("%s\n"
	       "    id: %u\n"
	       "    priority: %u\n"
	       "    default_priority: %u\n"
	       "    remaining attempts: %u\n"
	       "    default attempts: %u\n"
	       "    boot: '%s'\n",
	       target->name, target->id, target->priority, target->default_priority,
	       target->remaining_attempts, target->default_attempts,
	       target->boot);
	if (!ok)
		printf("    disabled due to %s\n", reason);
}

static int pr_setenv(struct bootchooser *bc, const char *fmt, ...)
{
	va_list ap;
	int ret = 0;
	char *str, *val;
	const char *oldval;

	va_start(ap, fmt);
	str = bvasprintf(fmt, ap);
	va_end(ap);

	if (!str)
		return -ENOMEM;

	val = parse_assignment(str);
	if (!val) {
		ret = -EINVAL;
		goto err;
	}

	oldval = getenv(str);
	if (!oldval || strcmp(oldval, val)) {
		if (bc->state)
			ret = setenv(str, val);
		else
			ret = nvvar_add(str, val);
	}

err:
	free(str);

	return ret;
}

static const char *pr_getenv(const char *fmt, ...)
{
	va_list ap;
	char *str;
	const char *val;

	va_start(ap, fmt);
	str = bvasprintf(fmt, ap);
	va_end(ap);

	if (!str)
		return NULL;

	val = getenv(str);

	free(str);

	return val;
}

static int getenv_u32(const char *prefix, const char *name, uint32_t *retval)
{
	char *str;
	const char *val;

	str = xasprintf("%s.%s", prefix, name);

	val = getenv(str);

	free(str);

	if (!val)
		return -ENOENT;

	*retval = simple_strtoul(val, NULL, 0);

	return 0;
}

static int bootchooser_target_compare(struct list_head *a, struct list_head *b)
{
	struct bootchooser_target *bootchooser_a =
		list_entry(a, struct bootchooser_target, list);
	struct bootchooser_target *bootchooser_b =
		list_entry(b, struct bootchooser_target, list);

	/* order with descending priority */
	return bootchooser_a->priority >= bootchooser_b->priority ? -1 : 1;
}

/**
 * bootchooser_target_new - Create a new bootchooser target
 * @bc: The bootchooser
 * @name: The name of the new target
 *
 * Parses the variables associated with @name, creates a bootchooser
 * target from it and returns it.
 */
static struct bootchooser_target *bootchooser_target_new(struct bootchooser *bc,
							 const char *name)
{
	struct bootchooser_target *target = xzalloc(sizeof(*target));
	const char *val;
	int ret;

	target->name = xstrdup(name);
	target->prefix = basprintf("%s.%s", BOOTCHOOSER_PREFIX, name);
	target->state_prefix = basprintf("%s.%s", bc->state_prefix, name);
	target->default_attempts = global_default_attempts;
	target->default_priority = global_default_priority;

	getenv_u32(target->prefix, "default_priority",
			    &target->default_priority);
	getenv_u32(target->prefix, "default_attempts",
			    &target->default_attempts);

	ret = getenv_u32(target->state_prefix, "priority", &target->priority);
	if (ret) {
		pr_warn("Cannot read priority for target %s, using default %d\n",
		       target->name, target->default_priority);
		target->priority = target->default_priority;
	}

	ret = getenv_u32(target->state_prefix, "remaining_attempts", &target->remaining_attempts);
	if (ret) {
		pr_warn("Cannot read remaining attempts for target %s, using default %d\n",
		       target->name, target->default_attempts);
		target->remaining_attempts = target->default_attempts;
	}

	if (target->remaining_attempts && !target->priority) {
		pr_warn("Disabled target %s has remaining attempts %d, setting to 0\n",
			target->name, target->remaining_attempts);
		target->remaining_attempts = 0;
	}

	val = pr_getenv("%s.boot", target->prefix);
	if (!val)
		val = target->name;
	target->boot = xstrdup(val);

	return target;
}

/**
 * bootchooser_target_by_id - Return a target given its id
 *
 * Each target has an id, simply counted by the order they appear in
 * global.bootchooser.targets. We start counting at one to leave 0
 * for detection of uninitialized variables.
 */
static struct bootchooser_target *bootchooser_target_by_id(struct bootchooser *bc,
							   uint32_t id)
{
	struct bootchooser_target *target;

	list_for_each_entry(target, &bc->targets, list)
		if (target->id == id)
			return target;

	return NULL;
}

/**
 * bootchooser_target_disable - Disable a bootchooser target
 */
static void bootchooser_target_disable(struct bootchooser_target *target)
{
	target->priority = 0;
	target->remaining_attempts = 0;
}

/**
 * bootchooser_target_enabled - test if a target is enabled
 *
 * Returns true if a target is enabled, false if it's not.
 */
static bool bootchooser_target_enabled(struct bootchooser_target *target)
{
	return target->priority != 0;
}

/**
 * bootchooser_reset_attempts - reset remaining attempts of targets
 *
 * Reset the remaining_attempts counter of all enabled targets
 * to their default values.
 */
static void bootchooser_reset_attempts(struct bootchooser *bc)
{
	struct bootchooser_target *target;

	bootchooser_for_each_target(bc, target) {
		if (bootchooser_target_enabled(target))
			bootchooser_target_set_attempts(target, -1);
	}
}

/**
 * bootchooser_reset_priorities - reset priorities of targets
 *
 * Reset the priorities counter of all targets to their default
 * values.
 */
static void bootchooser_reset_priorities(struct bootchooser *bc)
{
	struct bootchooser_target *target;

	bootchooser_for_each_target(bc, target)
		bootchooser_target_set_priority(target, -1);
}

static struct bootchooser *bootchooser;

/**
 * bootchooser_get - get a reference to the bootchooser
 *
 * When no bootchooser is initialized this function allocates the bootchooser
 * and initializes it with the different globalvars and state variables. The
 * bootchooser is returned. Subsequent calls will return a reference to the same
 * bootchooser.
 */
struct bootchooser *bootchooser_get(void)
{
	struct bootchooser *bc;
	struct bootchooser_target *target;
	char *targets, *str, *freep = NULL, *delim;
	int ret = -EINVAL, id = 1;
	uint32_t last_chosen;
	static int attempts_resetted;

	if (bootchooser) {
		bootchooser->refs++;
		return bootchooser;
	}

	bc = xzalloc(sizeof(*bc));

	if (*state_prefix) {
		if (IS_ENABLED(CONFIG_STATE)) {
			char *state_devname;

			delim = strchr(state_prefix, '.');
			if (delim)
				state_devname = xstrndup(state_prefix, delim - state_prefix);
			else
				state_devname = xstrdup(state_prefix);
			bc->state_prefix = xstrdup(state_prefix);
			bc->state = state_by_name(state_devname);
			if (!bc->state) {
				pr_err("Cannot get state '%s'\n",
				       state_devname);
				free(state_devname);
				ret = -ENODEV;
				goto err;
			}
			free(state_devname);
		} else {
			pr_err("State disabled, cannot use nv.state_prefix=%s\n",
			       state_prefix);
			ret = -ENODEV;
			goto err;
		}
	} else {
		bc->state_prefix = xstrdup("nv.bootchooser");
	}

	INIT_LIST_HEAD(&bc->targets);

	freep = targets = xstrdup(available_targets);

	while (1) {
		str = strsep(&targets, " ");
		if (!str || !*str)
			break;

		target = bootchooser_target_new(bc, str);
		if (!IS_ERR(target)) {
			target->id = id;
			list_add_sort(&target->list, &bc->targets,
				      bootchooser_target_compare);
		}

		id++;
	}

	if (id == 1) {
		pr_err("Target list $global.bootchooser.targets is empty\n");
		goto err;
	}

	if (list_empty(&bc->targets)) {
		pr_err("No targets could be initialized\n");
		goto err;
	}

	free(freep);

	if (test_bit(RESET_PRIORITIES_ALL_ZERO, &reset_priorities)) {
		int priority = 0;

		bootchooser_for_each_target(bc, target)
			priority += target->priority;

		if (!priority) {
			pr_info("All targets disabled, re-enabling them\n");
			bootchooser_reset_priorities(bc);
		}
	}

	if (test_bit(RESET_ATTEMPTS_POWER_ON, &reset_attempts) &&
	    reset_source_get() == RESET_POR && !attempts_resetted) {
		pr_info("Power-on Reset, resetting remaining attempts\n");
		bootchooser_reset_attempts(bc);
		attempts_resetted = 1;
	}

	if (test_bit(RESET_ATTEMPTS_ALL_ZERO, &reset_attempts)) {
		int attempts = 0;

		bootchooser_for_each_target(bc, target)
			attempts += target->remaining_attempts;

		if (!attempts) {
			pr_info("All enabled targets have 0 remaining attempts, resetting them\n");
			bootchooser_reset_attempts(bc);
		}
	}

	ret = getenv_u32(bc->state_prefix, "last_chosen", &last_chosen);
	if (!ret && last_chosen > 0) {
		bc->last_chosen = bootchooser_target_by_id(bc, last_chosen);
		if (!bc->last_chosen)
			pr_warn("Last booted target with id %d does not exist\n", last_chosen);
	}

	if (bc->last_chosen && last_boot_successful)
		bootchooser_target_set_attempts(bc->last_chosen, -1);

	if (disable_on_zero_attempts) {
		bootchooser_for_each_target(bc, target) {
			if (!target->remaining_attempts) {
				pr_info("target %s has 0 remaining attempts, disabling\n",
					target->name);
				bootchooser_target_disable(target);
			}
		}

	}

	bootchooser = bc;

	bootchooser->refs = 1;

	return bc;

err:
	free(freep);
	free(bc);

	return ERR_PTR(ret);
}

/**
 * bootchooser_save - save a bootchooser to the backing store
 * @bc:		The bootchooser instance to save
 *
 * Return: 0 for success, negative error code otherwise
 */
int bootchooser_save(struct bootchooser *bc)
{
	struct bootchooser_target *target;
	int ret;

	if (bc->last_chosen)
		pr_setenv(bc, "%s.last_chosen=%d", bc->state_prefix,
			  bc->last_chosen->id);

	list_for_each_entry(target, &bc->targets, list) {
		ret = pr_setenv(bc, "%s.remaining_attempts=%d",
				target->state_prefix,
				target->remaining_attempts);
		if (ret)
			return ret;

		ret = pr_setenv(bc, "%s.priority=%d",
				target->state_prefix, target->priority);
		if (ret)
			return ret;
	}

	if (IS_ENABLED(CONFIG_STATE) && bc->state) {
		ret = state_save(bc->state);
		if (ret) {
			pr_err("Cannot save state: %s\n", strerror(-ret));
			return ret;
		}
	} else {
		ret = nvvar_save();
		if (ret) {
			pr_err("Cannot save nv variables: %s\n", strerror(-ret));
			return ret;
		}
	}

	return 0;
}

/**
 * bootchooser_put - return a bootchooser reference
 * @bc: The bootchooser instance
 *
 * This returns a reference to the bootchooser. If it is the last reference the
 * bootchooser is saved and the associated memory is freed.
 *
 * Return: 0 for success or a negative error code. An error can occur when
 *         bootchooser_save fails to write to the storage, nevertheless the
 *         bootchooser reference is still released.
 */
int bootchooser_put(struct bootchooser *bc)
{
	struct bootchooser_target *target, *tmp;
	int ret;

	bc->refs--;

	if (bc->refs)
		return 0;

	ret = bootchooser_save(bc);
	if (ret)
		pr_err("Failed to save bootchooser state: %s\n", strerror(-ret));

	list_for_each_entry_safe(target, tmp, &bc->targets, list) {
		free(target->boot);
		free(target->prefix);
		free(target->state_prefix);
		free(target->name);
		free(target);
	}

	free(bc);

	bootchooser = NULL;

	return ret;
}

/**
 * bootchooser_info - Show information about a bootchooser instance
 * @bc: The bootchooser
 */
void bootchooser_info(struct bootchooser *bc)
{
	struct bootchooser_target *target;
	const char *reason;
	int count = 0;

	printf("Good targets (first will be booted next):\n");
	list_for_each_entry(target, &bc->targets, list) {
		if (bootchooser_target_ok(target, NULL)) {
			count++;
			pr_target(target);
		}
	}

	if (!count)
		printf("none\n");

	count = 0;

	printf("\nDisabled targets:\n");
	list_for_each_entry(target, &bc->targets, list) {
		if (!bootchooser_target_ok(target, &reason)) {
			count++;
			pr_target(target);
		}
	}

	if (!count)
		printf("none\n");

	printf("\nlast booted target: %s\n", bc->last_chosen ?
	       bc->last_chosen->name : "unknown");
}

/**
 * bootchooser_get_target - get the target that shall be booted next
 * @bc:		The bootchooser
 *
 * This is the heart of the bootchooser. This function selects the next
 * target to boot and returns it. The remaining_attempts counter of the
 * selected target is decreased and the bootchooser state is saved to the
 * backend.
 *
 * Return: The next target
 */
static struct bootchooser_target *bootchooser_get_target(struct bootchooser *bc)
{
	struct bootchooser_target *target;

	list_for_each_entry(target, &bc->targets, list) {
		if (bootchooser_target_ok(target, NULL))
			goto found;
	}

	pr_err("No valid targets found:\n");
	list_for_each_entry(target, &bc->targets, list)
		pr_target(target);

	return ERR_PTR(-ENOENT);

found:
	target->remaining_attempts--;

	if (bc->verbose)
		pr_info("name=%s decrementing remaining_attempts to %d\n",
			target->name, target->remaining_attempts);

	if (bc->verbose)
		pr_info("selected target '%s', boot '%s'\n", target->name, target->boot);

	bc->last_chosen = target;

	bootchooser_save(bc);

	return target;
}

/**
 * bootchooser_target_name - get the name of a target
 * @target:	The target
 *
 * Given a bootchooser target this function returns its name.
 *
 * Return: The name of the target
 */
const char *bootchooser_target_name(struct bootchooser_target *target)
{
	return target->name;
}

/**
 * bootchooser_target_by_name - get a target from name
 * @bc:		The bootchooser
 * @name:	The name of the target to retrieve
 *
 * Given a name this function returns the corresponding target.
 *
 * Return: The target if found, NULL otherwise
 */
struct bootchooser_target *bootchooser_target_by_name(struct bootchooser *bc,
						      const char *name)
{
	struct bootchooser_target *target;

	bootchooser_for_each_target(bc, target)
		if (!strcmp(target->name, name))
			return target;
	return NULL;
}

/**
 * bootchooser_target_set_attempts - set remaining attempts of a target
 * @target:	The target to change
 * @attempts:	The number of attempts
 *
 * This sets the number of remaining attempts for a bootchooser target.
 * If @attempts is < 0 then the remaining attempts is reset to the default
 * value.
 *
 * Return: 0 for success, negative error code otherwise
 */
int bootchooser_target_set_attempts(struct bootchooser_target *target, int attempts)
{
	if (attempts >= 0)
		target->remaining_attempts = attempts;
	else
		target->remaining_attempts = target->default_attempts;

	return 0;
}

/**
 * bootchooser_target_set_priority - set priority of a target
 * @target:	The target to change
 * @priority:	The priority
 *
 * This sets the priority of a bootchooser target. If @priority is < 0
 * then the priority reset to the default value.
 *
 * Return: 0 for success, negative error code otherwise
 */
int bootchooser_target_set_priority(struct bootchooser_target *target, int priority)
{
	if (priority >= 0)
		target->priority = priority;
	else
		target->priority = target->default_priority;

	return 0;
}

/**
 * bootchooser_target_first - get the first target from a bootchooser
 * @bc:		The bootchooser
 *
 * Gets the first target from a bootchooser, used for the bootchooser
 * target iterator.
 *
 * Return: The first target or NULL if no target exists
 */
struct bootchooser_target *bootchooser_target_first(struct bootchooser *bc)
{
	return list_first_entry_or_null(&bc->targets,
					struct bootchooser_target, list);
}

/**
 * bootchooser_target_next - get the next target from a bootchooser
 * @bc:		The bootchooser
 * @target:	The current target
 *
 * Gets the next target from a bootchooser, used for the bootchooser
 * target iterator.
 *
 * Return: The first target or NULL if no more targets exist
 */
struct bootchooser_target *bootchooser_target_next(struct bootchooser *bc,
					       struct bootchooser_target *target)
{
	struct list_head *next = target->list.next;

	if (next == &bc->targets)
		return NULL;

	return list_entry(next, struct bootchooser_target, list);
}

/**
 * bootchooser_last_boot_successful - tell that the last boot was successful
 *
 * This tells bootchooser that the last boot was successful.
 */
void bootchooser_last_boot_successful(void)
{
	last_boot_successful = true;
}

/**
 * bootchooser_get_last_chosen - get the target which was chosen last time
 * @bc:		The bootchooser
 *
 * Bootchooser stores the id of the target which was last booted in
 * <state_prefix>.last_chosen. This function returns the target associated
 * with this id.
 *
 * Return: The target which was booted last time
 */
struct bootchooser_target *bootchooser_get_last_chosen(struct bootchooser *bc)
{
	if (!bc->last_chosen)
		return ERR_PTR(-ENODEV);

	return bc->last_chosen;
}

static int bootchooser_boot_one(struct bootchooser *bc, int *tryagain)
{
	char *system;
	struct bootentries *entries;
	struct bootentry *entry;
	struct bootchooser_target *target;
	int ret = 0;

	entries = bootentries_alloc();

	target = bootchooser_get_target(bc);
	if (IS_ERR(target)) {
		ret = PTR_ERR(target);
		*tryagain = 0;
		goto out;
	}

	system = basprintf("bootchooser.active=%s", target->name);
	globalvar_add_simple("linux.bootargs.bootchooser", system);
	free(system);

	ret = bootentry_create_from_name(entries, target->boot);
	if (ret <= 0) {
		printf("Nothing bootable found on '%s'\n", target->boot);
		*tryagain = 1;
		ret = -ENODEV;
		goto out;
	}

	last_boot_successful = false;

	ret = -ENOENT;

	bootentries_for_each_entry(entries, entry) {
		ret = boot_entry(entry, bc->verbose, bc->dryrun);
		if (!ret) {
			*tryagain = 0;
			goto out;
		}
	}

	*tryagain = 1;
out:
	globalvar_set_match("linux.bootargs.bootchooser", NULL);

	bootentries_free(entries);

	return ret;
}

int bootchooser_boot(struct bootchooser *bc)
{
	int ret, tryagain;

	do {
		ret = bootchooser_boot_one(bc, &tryagain);

		if (!retry)
			break;
	} while (tryagain);

	return ret;
}

static int bootchooser_entry_boot(struct bootentry *entry, int verbose, int dryrun)
{
	struct bootchooser *bc;
	int ret;

	bc = bootchooser_get();
	if (IS_ERR(bc))
		return PTR_ERR(bc);

	bc->verbose = verbose;
	bc->dryrun = dryrun;

	ret = bootchooser_boot(bc);

	bootchooser_put(bc);

	return ret;
}

static void bootchooser_release(struct bootentry *entry)
{
	free(entry);
}

/**
 * bootchooser_create_bootentry - create a boot entry
 * @entries:	The list of bootentries
 *
 * This adds a bootchooser to the list of boot entries. Called
 * by the 'boot' code.
 *
 * Return: The number of entries added to the list
 */
static int bootchooser_add_entry(struct bootentries *entries, const char *name)
{
	struct bootchooser *bc;
	struct bootentry *entry;

	if (strcmp(name, "bootchooser"))
		return 0;

	bc = bootchooser_get();
	if (IS_ERR(bc))
		return PTR_ERR(bc);

	entry = xzalloc(sizeof(*entry));

	entry->boot = bootchooser_entry_boot;
	entry->release = bootchooser_release;
	entry->title = xstrdup("bootchooser");
	entry->description = xstrdup("bootchooser");

	bootentries_add_entry(entries, entry);

	bootchooser_put(bc);

	return 1;
}

static const char * const reset_attempts_names[] = {
	[RESET_ATTEMPTS_POWER_ON] = "power-on",
	[RESET_ATTEMPTS_ALL_ZERO] = "all-zero",
};

static const char * const reset_priorities_names[] = {
	[RESET_PRIORITIES_ALL_ZERO] = "all-zero",
};

static int bootchooser_init(void)
{
	state_prefix = xstrdup("");
	available_targets = xstrdup("");

	globalvar_add_simple_bool("bootchooser.disable_on_zero_attempts", &disable_on_zero_attempts);
	globalvar_add_simple_bool("bootchooser.retry", &retry);
	globalvar_add_simple_string("bootchooser.targets", &available_targets);
	globalvar_add_simple_string("bootchooser.state_prefix", &state_prefix);
	globalvar_add_simple_int("bootchooser.default_attempts", &global_default_attempts, "%u");
	globalvar_add_simple_int("bootchooser.default_priority", &global_default_priority, "%u");
	globalvar_add_simple_bitmask("bootchooser.reset_attempts", &reset_attempts,
				  reset_attempts_names, ARRAY_SIZE(reset_attempts_names));
	globalvar_add_simple_bitmask("bootchooser.reset_priorities", &reset_priorities,
				  reset_priorities_names, ARRAY_SIZE(reset_priorities_names));

	bootentry_register_provider(bootchooser_add_entry);

	return 0;
}
device_initcall(bootchooser_init);

BAREBOX_MAGICVAR(global.bootchooser.disable_on_zero_attempts,
		 "bootchooser: Disable target when remaining attempts counter reaches 0");
BAREBOX_MAGICVAR(global.bootchooser.retry,
		 "bootchooser: Try again when booting a target fails");
BAREBOX_MAGICVAR(global.bootchooser.targets,
		 "bootchooser: Space separated list of target names");
BAREBOX_MAGICVAR(global.bootchooser.default_attempts,
		 "bootchooser: Default number of attempts for a target");
BAREBOX_MAGICVAR(global.bootchooser.default_priority,
		 "bootchooser: Default priority for a target");
BAREBOX_MAGICVAR(global.bootchooser.state_prefix,
		 "bootchooser: state name prefix, empty for nv backend");
