// SPDX-License-Identifier: GPL-2.0+
/*
 * Uclass for sandbox host interface, used to access files on the host which
 * contain partitions and filesystem
 *
 * Copyright 2022 Google LLC
 * Written by Simon Glass <sjg@chromium.org>
 */

#define LOG_CATEGORY UCLASS_HOST

#include <blk.h>
#include <dm.h>
#include <malloc.h>
#include <part.h>
#include <sandbox_host.h>
#include <dm/device-internal.h>
#include <dm/lists.h>
#include <dm/uclass-internal.h>

DECLARE_GLOBAL_DATA_PTR;

/**
 * struct host_priv - information kept by the host uclass
 *
 * @cur_dev: Currently selected host device, or NULL if none
 */
struct host_priv {
	struct udevice *cur_dev;
};

int host_create_device(const char *label, bool removable, unsigned long blksz,
		       struct udevice **devp)
{
	char dev_name[30], *str, *label_new;
	struct host_sb_plat *plat;
	struct udevice *dev, *blk;
	int ret;

	/* unbind any existing device with this label */
	dev = host_find_by_label(label);
	if (dev) {
		ret = host_detach_file(dev);
		if (ret)
			return log_msg_ret("det", ret);

		ret = device_unbind(dev);
		if (ret)
			return log_msg_ret("unb", ret);
	}

	snprintf(dev_name, sizeof(dev_name), "host-%s", label);
	str = strdup(dev_name);
	if (!str)
		return -ENOMEM;

	label_new = strdup(label);
	if (!label_new) {
		ret = -ENOMEM;
		goto no_label;
	}

	ret = device_bind_driver(gd->dm_root, "host_sb_drv", str, &dev);
	if (ret)
		goto no_dev;
	device_set_name_alloced(dev);

	if (!blk_find_from_parent(dev, &blk)) {
		struct blk_desc *desc = dev_get_uclass_plat(blk);

		desc->removable = removable;

		/* update blk device's block size with the provided one */
		if (blksz != desc->blksz) {
			desc->blksz = blksz;
			desc->log2blksz = LOG2(desc->blksz);
		}
	}

	plat = dev_get_plat(dev);
	plat->label = label_new;
	*devp = dev;

	return 0;

no_dev:
	free(label_new);
no_label:
	free(str);

	return ret;
}

int host_attach_file(struct udevice *dev, const char *filename)
{
	struct host_ops *ops = host_get_ops(dev);

	if (!ops->attach_file)
		return -ENOSYS;

	return ops->attach_file(dev, filename);
}

int host_create_attach_file(const char *label, const char *filename,
			    bool removable, unsigned long blksz,
			    struct udevice **devp)
{
	struct udevice *dev;
	int ret;

	ret = host_create_device(label, removable, blksz, &dev);
	if (ret)
		return log_msg_ret("cre", ret);

	ret = host_attach_file(dev, filename);
	if (ret) {
		device_unbind(dev);
		return log_msg_ret("att", ret);
	}
	*devp = dev;

	return 0;
}

int host_detach_file(struct udevice *dev)
{
	struct host_ops *ops = host_get_ops(dev);

	if (!ops->detach_file)
		return -ENOSYS;

	if (dev == host_get_cur_dev())
		host_set_cur_dev(NULL);

	return ops->detach_file(dev);
}

struct udevice *host_find_by_label(const char *label)
{
	struct udevice *dev;
	struct uclass *uc;

	uclass_id_foreach_dev(UCLASS_HOST, dev, uc) {
		struct host_sb_plat *plat = dev_get_plat(dev);

		if (plat->label && !strcmp(label, plat->label))
			return dev;
	}

	return NULL;
}

struct udevice *host_get_cur_dev(void)
{
	struct uclass *uc = uclass_find(UCLASS_HOST);

	if (uc) {
		struct host_priv *priv = uclass_get_priv(uc);

		return priv->cur_dev;
	}

	return NULL;
}

void host_set_cur_dev(struct udevice *dev)
{
	struct uclass *uc = uclass_find(UCLASS_HOST);

	if (uc) {
		struct host_priv *priv = uclass_get_priv(uc);

		priv->cur_dev = dev;
	}
}

UCLASS_DRIVER(host) = {
	.id		= UCLASS_HOST,
	.name		= "host",
#if CONFIG_IS_ENABLED(OF_REAL)
	.post_bind	= dm_scan_fdt_dev,
#endif
	.priv_auto	= sizeof(struct host_priv),
};
