// SPDX-License-Identifier: GPL-2.0+
/*
 * 'bootflow' command
 *
 * Copyright 2021 Google LLC
 * Written by Simon Glass <sjg@chromium.org>
 */

#include <common.h>
#include <bootdev.h>
#include <bootflow.h>
#include <bootstd.h>
#include <command.h>
#include <console.h>
#include <dm.h>
#include <mapmem.h>

/**
 * report_bootflow_err() - Report where a bootflow failed
 *
 * When a bootflow does not make it to the 'loaded' state, something went wrong.
 * Print a helpful message if there is an error
 *
 * @bflow: Bootflow to process
 * @err: Error code (0 if none)
 */
static void report_bootflow_err(struct bootflow *bflow, int err)
{
	if (!err)
		return;

	/* Indent out to 'Method' */
	printf("     ** ");

	switch (bflow->state) {
	case BOOTFLOWST_BASE:
		printf("No media/partition found");
		break;
	case BOOTFLOWST_MEDIA:
		printf("No partition found");
		break;
	case BOOTFLOWST_PART:
		printf("No filesystem found");
		break;
	case BOOTFLOWST_FS:
		printf("File not found");
		break;
	case BOOTFLOWST_FILE:
		printf("File cannot be loaded");
		break;
	case BOOTFLOWST_READY:
		printf("Ready");
		break;
	case BOOTFLOWST_COUNT:
		break;
	}

	printf(", err=%d\n", err);
}

/**
 * show_bootflow() - Show the status of a bootflow
 *
 * @seq: Bootflow index
 * @bflow: Bootflow to show
 * @errors: True to show the error received, if any
 */
static void show_bootflow(int index, struct bootflow *bflow, bool errors)
{
	printf("%3x  %-11s  %-6s  %-9.9s %4x  %-25.25s %s\n", index,
	       bflow->method->name, bootflow_state_get_name(bflow->state),
	       bflow->dev ? dev_get_uclass_name(dev_get_parent(bflow->dev)) :
	       "(none)", bflow->part, bflow->name, bflow->fname);
	if (errors)
		report_bootflow_err(bflow, bflow->err);
}

static void show_header(void)
{
	printf("Seq  Method       State   Uclass    Part  Name                      Filename\n");
	printf("---  -----------  ------  --------  ----  ------------------------  ----------------\n");
}

static void show_footer(int count, int num_valid)
{
	printf("---  -----------  ------  --------  ----  ------------------------  ----------------\n");
	printf("(%d bootflow%s, %d valid)\n", count, count != 1 ? "s" : "",
	       num_valid);
}

static int do_bootflow_scan(struct cmd_tbl *cmdtp, int flag, int argc,
			    char *const argv[])
{
	struct bootstd_priv *std;
	struct bootflow_iter iter;
	struct udevice *dev = NULL;
	struct bootflow bflow;
	bool all = false, boot = false, errors = false, no_global = false;
	bool list = false, no_hunter = false;
	int num_valid = 0;
	const char *label = NULL;
	bool has_args;
	int ret, i;
	int flags;

	ret = bootstd_get_priv(&std);
	if (ret)
		return CMD_RET_FAILURE;

	has_args = argc > 1 && *argv[1] == '-';
	if (IS_ENABLED(CONFIG_CMD_BOOTFLOW_FULL)) {
		if (has_args) {
			all = strchr(argv[1], 'a');
			boot = strchr(argv[1], 'b');
			errors = strchr(argv[1], 'e');
			no_global = strchr(argv[1], 'G');
			list = strchr(argv[1], 'l');
			no_hunter = strchr(argv[1], 'H');
			argc--;
			argv++;
		}
		if (argc > 1)
			label = argv[1];
		if (!label)
			dev = std->cur_bootdev;
	} else {
		if (has_args) {
			printf("Flags not supported: enable CONFIG_BOOTFLOW_FULL\n");
			return CMD_RET_USAGE;
		}
		boot = true;
	}

	std->cur_bootflow = NULL;

	flags = 0;
	if (list)
		flags |= BOOTFLOWIF_SHOW;
	if (all)
		flags |= BOOTFLOWIF_ALL;
	if (no_global)
		flags |= BOOTFLOWIF_SKIP_GLOBAL;
	if (!no_hunter)
		flags |= BOOTFLOWIF_HUNT;

	/*
	 * If we have a device, just scan for bootflows attached to that device
	 */
	if (list) {
		printf("Scanning for bootflows ");
		if (dev)
			printf("in bootdev '%s'\n", dev->name);
		else if (label)
			printf("with label '%s'\n", label);
		else
			printf("in all bootdevs\n");
		show_header();
	}
	if (dev)
		bootdev_clear_bootflows(dev);
	else
		bootstd_clear_glob();
	for (i = 0,
	     ret = bootflow_scan_first(dev, label, &iter, flags, &bflow);
	     i < 1000 && ret != -ENODEV;
	     i++, ret = bootflow_scan_next(&iter, &bflow)) {
		bflow.err = ret;
		if (!ret)
			num_valid++;
		ret = bootdev_add_bootflow(&bflow);
		if (ret) {
			printf("Out of memory\n");
			return CMD_RET_FAILURE;
		}
		if (list)
			show_bootflow(i, &bflow, errors);
		if (boot && !bflow.err)
			bootflow_run_boot(&iter, &bflow);
	}
	bootflow_iter_uninit(&iter);
	if (list)
		show_footer(i, num_valid);

	return 0;
}

#ifdef CONFIG_CMD_BOOTFLOW_FULL
static int do_bootflow_list(struct cmd_tbl *cmdtp, int flag, int argc,
			    char *const argv[])
{
	struct bootstd_priv *std;
	struct udevice *dev;
	struct bootflow *bflow;
	int num_valid = 0;
	bool errors = false;
	int ret, i;

	if (argc > 1 && *argv[1] == '-')
		errors = strchr(argv[1], 'e');

	ret = bootstd_get_priv(&std);
	if (ret)
		return CMD_RET_FAILURE;
	dev = std->cur_bootdev;

	/* If we have a device, just list bootflows attached to that device */
	if (dev) {
		printf("Showing bootflows for bootdev '%s'\n", dev->name);
		show_header();
		for (ret = bootdev_first_bootflow(dev, &bflow), i = 0;
		     !ret;
		     ret = bootdev_next_bootflow(&bflow), i++) {
			num_valid += bflow->state == BOOTFLOWST_READY;
			show_bootflow(i, bflow, errors);
		}
	} else {
		printf("Showing all bootflows\n");
		show_header();
		for (ret = bootflow_first_glob(&bflow), i = 0;
		     !ret;
		     ret = bootflow_next_glob(&bflow), i++) {
			num_valid += bflow->state == BOOTFLOWST_READY;
			show_bootflow(i, bflow, errors);
		}
	}
	show_footer(i, num_valid);

	return 0;
}

static int do_bootflow_select(struct cmd_tbl *cmdtp, int flag, int argc,
			      char *const argv[])
{
	struct bootstd_priv *std;
	struct bootflow *bflow, *found;
	struct udevice *dev;
	const char *name;
	char *endp;
	int seq, i;
	int ret;

	ret = bootstd_get_priv(&std);
	if (ret)
		return CMD_RET_FAILURE;
;
	if (argc < 2) {
		std->cur_bootflow = NULL;
		return 0;
	}
	dev = std->cur_bootdev;

	name = argv[1];
	seq = simple_strtol(name, &endp, 16);
	found = NULL;

	/*
	 * If we have a bootdev device, only allow selection of bootflows
	 * attached to that device
	 */
	if (dev) {
		for (ret = bootdev_first_bootflow(dev, &bflow), i = 0;
		     !ret;
		     ret = bootdev_next_bootflow(&bflow), i++) {
			if (*endp ? !strcmp(bflow->name, name) : i == seq) {
				found = bflow;
				break;
			}
		}
	} else {
		for (ret = bootflow_first_glob(&bflow), i = 0;
		     !ret;
		     ret = bootflow_next_glob(&bflow), i++) {
			if (*endp ? !strcmp(bflow->name, name) : i == seq) {
				found = bflow;
				break;
			}
		}
	}

	if (!found) {
		printf("Cannot find bootflow '%s' ", name);
		if (dev)
			printf("in bootdev '%s' ", dev->name);
		printf("(err=%d)\n", ret);
		return CMD_RET_FAILURE;
	}
	std->cur_bootflow = found;

	return 0;
}

static int do_bootflow_info(struct cmd_tbl *cmdtp, int flag, int argc,
			    char *const argv[])
{
	struct bootstd_priv *std;
	struct bootflow *bflow;
	bool dump = false;
	int ret;

	if (argc > 1 && *argv[1] == '-')
		dump = strchr(argv[1], 'd');

	ret = bootstd_get_priv(&std);
	if (ret)
		return CMD_RET_FAILURE;

	if (!std->cur_bootflow) {
		printf("No bootflow selected\n");
		return CMD_RET_FAILURE;
	}
	bflow = std->cur_bootflow;

	printf("Name:      %s\n", bflow->name);
	printf("Device:    %s\n", bflow->dev->name);
	printf("Block dev: %s\n", bflow->blk ? bflow->blk->name : "(none)");
	printf("Method:    %s\n", bflow->method->name);
	printf("State:     %s\n", bootflow_state_get_name(bflow->state));
	printf("Partition: %d\n", bflow->part);
	printf("Subdir:    %s\n", bflow->subdir ? bflow->subdir : "(none)");
	printf("Filename:  %s\n", bflow->fname);
	printf("Buffer:    %lx\n", (ulong)map_to_sysmem(bflow->buf));
	printf("Size:      %x (%d bytes)\n", bflow->size, bflow->size);
	printf("OS:        %s\n", bflow->os_name ? bflow->os_name : "(none)");
	printf("Logo:      %s\n", bflow->logo ?
	       simple_xtoa((ulong)map_to_sysmem(bflow->logo)) : "(none)");
	if (bflow->logo) {
		printf("Logo size: %x (%d bytes)\n", bflow->logo_size,
		       bflow->logo_size);
	}
	printf("FDT:       %s\n", bflow->fdt_fname);
	if (bflow->fdt_fname) {
		printf("FDT size:  %x (%d bytes)\n", bflow->fdt_size,
		       bflow->fdt_size);
		printf("FDT addr:  %lx\n", bflow->fdt_addr);
	}
	printf("Error:     %d\n", bflow->err);
	if (dump && bflow->buf) {
		/* Set some sort of maximum on the size */
		int size = min(bflow->size, 10 << 10);
		int i;

		printf("Contents:\n\n");
		for (i = 0; i < size; i++) {
			putc(bflow->buf[i]);
			if (!(i % 128) && ctrlc()) {
				printf("...interrupted\n");
				break;
			}
		}
	}

	return 0;
}

static int do_bootflow_boot(struct cmd_tbl *cmdtp, int flag, int argc,
			    char *const argv[])
{
	struct bootstd_priv *std;
	struct bootflow *bflow;
	int ret;

	ret = bootstd_get_priv(&std);
	if (ret)
		return CMD_RET_FAILURE;

	/*
	 * Require a current bootflow. Users can use 'bootflow scan -b' to
	 * automatically scan and boot, if needed.
	 */
	if (!std->cur_bootflow) {
		printf("No bootflow selected\n");
		return CMD_RET_FAILURE;
	}
	bflow = std->cur_bootflow;
	ret = bootflow_run_boot(NULL, bflow);
	if (ret)
		return CMD_RET_FAILURE;

	return 0;
}

static int do_bootflow_menu(struct cmd_tbl *cmdtp, int flag, int argc,
			    char *const argv[])
{
	struct bootstd_priv *std;
	struct bootflow *bflow;
	bool text_mode = false;
	int ret;

	if (argc > 1 && *argv[1] == '-')
		text_mode = strchr(argv[1], 't');

	ret = bootstd_get_priv(&std);
	if (ret)
		return CMD_RET_FAILURE;

	if (IS_ENABLED(CONFIG_EXPO)) {
		ret = bootflow_menu_run(std, text_mode, &bflow);
		if (ret) {
			if (ret == -EAGAIN)
				printf("Nothing chosen\n");
			else
				printf("Menu failed (err=%d)\n", ret);
		}
	} else {
		printf("Menu not supported\n");
		ret = -ENOSYS;
	}
	if (ret)
		return CMD_RET_FAILURE;

	printf("Selected: %s\n", bflow->os_name ? bflow->os_name : bflow->name);
	std->cur_bootflow = bflow;

	return 0;
}
#endif /* CONFIG_CMD_BOOTFLOW_FULL */

#ifdef CONFIG_SYS_LONGHELP
static char bootflow_help_text[] =
#ifdef CONFIG_CMD_BOOTFLOW_FULL
	"scan [-abeGl] [bdev]  - scan for valid bootflows (-l list, -a all, -e errors, -b boot, -G no global)\n"
	"bootflow list [-e]             - list scanned bootflows (-e errors)\n"
	"bootflow select [<num>|<name>] - select a bootflow\n"
	"bootflow info [-d]             - show info on current bootflow (-d dump bootflow)\n"
	"bootflow boot                  - boot current bootflow (or first available if none selected)\n"
	"bootflow menu [-t]             - show a menu of available bootflows";
#else
	"scan - boot first available bootflow\n";
#endif
#endif /* CONFIG_SYS_LONGHELP */

U_BOOT_CMD_WITH_SUBCMDS(bootflow, "Boot flows", bootflow_help_text,
	U_BOOT_SUBCMD_MKENT(scan, 3, 1, do_bootflow_scan),
#ifdef CONFIG_CMD_BOOTFLOW_FULL
	U_BOOT_SUBCMD_MKENT(list, 2, 1, do_bootflow_list),
	U_BOOT_SUBCMD_MKENT(select, 2, 1, do_bootflow_select),
	U_BOOT_SUBCMD_MKENT(info, 2, 1, do_bootflow_info),
	U_BOOT_SUBCMD_MKENT(boot, 1, 1, do_bootflow_boot),
	U_BOOT_SUBCMD_MKENT(menu, 2, 1, do_bootflow_menu),
#endif
);