/*
 * Copyright (c) 2022 Google Inc
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include <stdlib.h>
#include <string.h>

#include <zephyr/devicetree.h>
#include <zephyr/drivers/bbram.h>
#include <zephyr/kernel.h>
#include <zephyr/shell/shell.h>
#include <zephyr/sys/util.h>

/* Buffer is only needed for bytes that follow command, device and address. */
#define BUF_ARRAY_CNT (CONFIG_SHELL_ARGC_MAX - 3)

static inline int parse_ul(const char *str, unsigned long *result)
{
	char *end;
	unsigned long val;

	val = strtoul(str, &end, 0);

	if (*str == '\0' || *end != '\0') {
		return -EINVAL;
	}

	*result = val;
	return 0;
}

static inline int parse_u32(const char *str, uint32_t *result)
{
	unsigned long val;

	if (parse_ul(str, &val) || val > 0xffffffff) {
		return -EINVAL;
	}
	*result = (uint32_t)val;
	return 0;
}

static inline int parse_u8(const char *str, uint8_t *result)
{
	unsigned long val;

	if (parse_ul(str, &val) || val > 0xff) {
		return -EINVAL;
	}
	*result = (uint8_t)val;
	return 0;
}

static inline int parse_device(const struct shell *sh, size_t argc, char *argv[],
			       const struct device **bbram_dev)
{
	if (argc < 2) {
		shell_error(sh, "Missing BBRAM device");
		return -EINVAL;
	}

	*bbram_dev = shell_device_get_binding(argv[1]);
	if (!*bbram_dev) {
		shell_error(sh, "Given BBRAM device was not found");
		return -ENODEV;
	}

	return 0;
}

static int cmd_read(const struct shell *sh, size_t argc, char *argv[])
{
	const struct device *bbram_dev;
	uint32_t addr;
	size_t size;
	int part_size, ret;

	ret = parse_device(sh, argc, argv, &bbram_dev);
	if (ret) {
		return ret;
	}

	if (argc < 3) {
		/* Dump whole BBRAM if address not provided. */
		addr = 0;
		ret = bbram_get_size(bbram_dev, &size);
		if (ret < 0) {
			shell_error(sh, "Can't get BBRAM size: %d", ret);
			return -EIO;
		}
	} else {
		/* Parse address if provided. */
		ret = parse_u32(argv[2], &addr);
		if (ret) {
			return ret;
		}

		/* If size not provided read one byte. */
		size = 1;

		if (argc >= 4) {
			/* Parse size if provided. */
			ret = parse_u32(argv[3], &size);
			if (ret) {
				return ret;
			}
		}
	}

	for (int cnt = 0; cnt < size; cnt += part_size) {
		uint8_t data[SHELL_HEXDUMP_BYTES_IN_LINE];

		part_size = MIN(size - cnt, SHELL_HEXDUMP_BYTES_IN_LINE);
		ret = bbram_read(bbram_dev, addr, part_size, data);
		if (ret != 0) {
			shell_error(sh, "BBRAM read error: %d", ret);
			return -EIO;
		}
		shell_hexdump_line(sh, addr, data, part_size);
		addr += part_size;
	}

	shell_print(sh, "");
	return 0;
}

static int cmd_write(const struct shell *sh, size_t argc, char *argv[])
{
	const struct device *bbram_dev;
	uint8_t buf[BUF_ARRAY_CNT];
	uint32_t addr;
	size_t size = 0;
	int ret;

	ret = parse_device(sh, argc, argv, &bbram_dev);
	if (ret) {
		return ret;
	}

	if (argc < 3) {
		shell_error(sh, "Missing address");
		return -EINVAL;
	}

	/* Parse address. */
	ret = parse_u32(argv[2], &addr);
	if (ret) {
		return ret;
	}

	/* Parse bytes and place them in the buffer. */
	for (int i = 3; i < argc; i++) {
		ret = parse_u8(argv[i], &buf[i - 3]);
		if (ret) {
			return ret;
		}
		size++;
	}

	if (size == 0) {
		shell_error(sh, "Missing data");
		return -EINVAL;
	}

	ret = bbram_write(bbram_dev, addr, size, buf);
	if (ret < 0) {
		shell_error(sh, "BBRAM write error: %d", ret);
		return -EIO;
	}

	return 0;
}

static void device_name_get(size_t idx, struct shell_static_entry *entry)
{
	const struct device *dev = shell_device_lookup(idx, NULL);

	entry->syntax = (dev != NULL) ? dev->name : NULL;
	entry->handler = NULL;
	entry->help = NULL;
	entry->subcmd = NULL;
}

SHELL_DYNAMIC_CMD_CREATE(dsub_device_name, device_name_get);

SHELL_STATIC_SUBCMD_SET_CREATE(bbram_cmds,
			       SHELL_CMD_ARG(read, &dsub_device_name,
					     "<device> [<address>] [<count>]", cmd_read, 2, 2),
			       SHELL_CMD_ARG(write, &dsub_device_name,
					     "<device> <address> <byte> [<byte>...]", cmd_write, 4,
					     BUF_ARRAY_CNT),
			       SHELL_SUBCMD_SET_END);

static int cmd_bbram(const struct shell *sh, size_t argc, char **argv)
{
	shell_error(sh, "%s: unknown parameter: %s", argv[0], argv[1]);
	return -EINVAL;
}

SHELL_CMD_ARG_REGISTER(bbram, &bbram_cmds, "Battery-backed RAM shell commands", cmd_bbram, 2, 0);
