// SPDX-License-Identifier: BSD-2-Clause
/*
 * fdt_domain.c - Flat Device Tree Domain helper routines
 *
 * Copyright (c) 2020 Western Digital Corporation or its affiliates.
 *
 * Authors:
 *   Anup Patel <anup.patel@wdc.com>
 */

#include <libfdt.h>
#include <libfdt_env.h>
#include <sbi/sbi_domain.h>
#include <sbi/sbi_error.h>
#include <sbi/sbi_hartmask.h>
#include <sbi/sbi_heap.h>
#include <sbi/sbi_scratch.h>
#include <sbi_utils/fdt/fdt_domain.h>
#include <sbi_utils/fdt/fdt_helper.h>

int fdt_iterate_each_domain(void *fdt, void *opaque,
			    int (*fn)(void *fdt, int domain_offset,
				       void *opaque))
{
	int rc, doffset, poffset;

	if (!fdt || !fn)
		return SBI_EINVAL;

	poffset = fdt_path_offset(fdt, "/chosen");
	if (poffset < 0)
		return 0;
	poffset = fdt_node_offset_by_compatible(fdt, poffset,
						"opensbi,domain,config");
	if (poffset < 0)
		return 0;

	fdt_for_each_subnode(doffset, fdt, poffset) {
		if (fdt_node_check_compatible(fdt, doffset,
					      "opensbi,domain,instance"))
			continue;

		rc = fn(fdt, doffset, opaque);
		if (rc)
			return rc;
	}

	return 0;
}

static int fdt_iterate_each_domain_ro(const void *fdt, void *opaque,
				      int (*fn)(const void *fdt, int domain_offset,
						void *opaque))
{
	return fdt_iterate_each_domain((void *)fdt, opaque,
				       (int (*)(void *, int,  void *))fn);
}

int fdt_iterate_each_memregion(void *fdt, int domain_offset, void *opaque,
			       int (*fn)(void *fdt, int domain_offset,
					 int region_offset, u32 region_access,
					 void *opaque))
{
	u32 i, rcount;
	int rc, len, region_offset;
	const u32 *regions;

	if (!fdt || (domain_offset < 0) || !fn)
		return SBI_EINVAL;

	if (fdt_node_check_compatible(fdt, domain_offset,
				      "opensbi,domain,instance"))
		return SBI_EINVAL;

	regions = fdt_getprop(fdt, domain_offset, "regions", &len);
	if (!regions)
		return 0;

	rcount = (u32)len / (sizeof(u32) * 2);
	for (i = 0; i < rcount; i++) {
		region_offset = fdt_node_offset_by_phandle(fdt,
						fdt32_to_cpu(regions[2 * i]));
		if (region_offset < 0)
			return region_offset;

		if (fdt_node_check_compatible(fdt, region_offset,
					      "opensbi,domain,memregion"))
			return SBI_EINVAL;

		rc = fn(fdt, domain_offset, region_offset,
			fdt32_to_cpu(regions[(2 * i) + 1]), opaque);
		if (rc)
			return rc;
	}

	return 0;
}

static int fdt_iterate_each_memregion_ro(const void *fdt, int domain_offset, void *opaque,
					 int (*fn)(const void *fdt, int domain_offset,
						   int region_offset, u32 region_access,
						   void *opaque))
{
	return fdt_iterate_each_memregion((void *)fdt, domain_offset, opaque,
					  (int (*)(void *, int, int, u32, void *))fn);
}

struct __fixup_find_domain_offset_info {
	const char *name;
	int *doffset;
};

static int __fixup_find_domain_offset(void *fdt, int doff, void *p)
{
	struct __fixup_find_domain_offset_info *fdo = p;

	if (!strncmp(fdo->name, fdt_get_name(fdt, doff, NULL), strlen(fdo->name)))
		*fdo->doffset = doff;

	return 0;
}

#define DISABLE_DEVICES_MASK	(SBI_DOMAIN_MEMREGION_READABLE | \
				 SBI_DOMAIN_MEMREGION_WRITEABLE | \
				 SBI_DOMAIN_MEMREGION_EXECUTABLE)

static int __fixup_count_disable_devices(void *fdt, int doff, int roff,
					 u32 perm, void *p)
{
	int len;
	u32 *dcount = p;

	if (perm & DISABLE_DEVICES_MASK)
		return 0;

	len = 0;
	if (fdt_getprop(fdt, roff, "devices", &len))
		*dcount += len / sizeof(u32);

	return 0;
}

static int __fixup_disable_devices(void *fdt, int doff, int roff,
				   u32 raccess, void *p)
{
	int i, len, coff;
	const u32 *devices;

	if (raccess & DISABLE_DEVICES_MASK)
		return 0;

	len = 0;
	devices = fdt_getprop(fdt, roff, "devices", &len);
	if (!devices)
		return 0;
	len = len / sizeof(u32);

	for (i = 0; i < len; i++) {
		coff = fdt_node_offset_by_phandle(fdt,
					fdt32_to_cpu(devices[i]));
		if (coff < 0)
			return coff;

		fdt_setprop_string(fdt, coff, "status", "disabled");
	}

	return 0;
}

void fdt_domain_fixup(void *fdt)
{
	u32 i, dcount;
	int err, poffset, doffset;
	struct sbi_domain *dom = sbi_domain_thishart_ptr();
	struct __fixup_find_domain_offset_info fdo;

	/* Remove the domain assignment DT property from CPU DT nodes */
	poffset = fdt_path_offset(fdt, "/cpus");
	if (poffset < 0)
		return;
	fdt_for_each_subnode(doffset, fdt, poffset) {
		err = fdt_parse_hart_id(fdt, doffset, &i);
		if (err)
			continue;

		if (!fdt_node_is_enabled(fdt, doffset))
			continue;

		fdt_nop_property(fdt, doffset, "opensbi-domain");
	}

	/* Skip device disable for root domain */
	if (!dom->index)
		goto skip_device_disable;

	/* Find current domain DT node */
	doffset = -1;
	fdo.name = dom->name;
	fdo.doffset = &doffset;
	fdt_iterate_each_domain(fdt, &fdo, __fixup_find_domain_offset);
	if (doffset < 0)
		goto skip_device_disable;

	/* Count current domain device DT nodes to be disabled */
	dcount = 0;
	fdt_iterate_each_memregion(fdt, doffset, &dcount,
				   __fixup_count_disable_devices);
	if (!dcount)
		goto skip_device_disable;

	/* Expand FDT based on device DT nodes to be disabled */
	err = fdt_open_into(fdt, fdt, fdt_totalsize(fdt) + dcount * 32);
	if (err < 0)
		return;

	/* Again find current domain DT node */
	doffset = -1;
	fdo.name = dom->name;
	fdo.doffset = &doffset;
	fdt_iterate_each_domain(fdt, &fdo, __fixup_find_domain_offset);
	if (doffset < 0)
		goto skip_device_disable;

	/* Disable device DT nodes for current domain */
	fdt_iterate_each_memregion(fdt, doffset, NULL,
				   __fixup_disable_devices);
skip_device_disable:

	/* Remove the OpenSBI domain config DT node */
	poffset = fdt_path_offset(fdt, "/chosen");
	if (poffset < 0)
		return;
	poffset = fdt_node_offset_by_compatible(fdt, poffset,
						"opensbi,domain,config");
	if (poffset < 0)
		return;
	fdt_nop_node(fdt, poffset);
}

#define FDT_DOMAIN_REGION_MAX_COUNT	16

struct parse_region_data {
	struct sbi_domain *dom;
	u32 region_count;
	u32 max_regions;
};

static int __fdt_parse_region(const void *fdt, int domain_offset,
			      int region_offset, u32 region_access,
			      void *opaque)
{
	int len;
	u32 val32;
	u64 val64;
	const u32 *val;
	unsigned long base, order, flags;
	struct parse_region_data *preg = opaque;

	/*
	 * Non-root domains cannot add a region with only M-mode
	 * access permissions. M-mode regions can only be part of
	 * root domain.
	 *
	 * SU permission bits can't be all zeroes when M-mode permission
	 * bits have at least one bit set.
	 */
	if (!(region_access & SBI_DOMAIN_MEMREGION_SU_ACCESS_MASK)
	    && (region_access & SBI_DOMAIN_MEMREGION_M_ACCESS_MASK))
		return SBI_EINVAL;

	/* Find next region of the domain */
	if (preg->max_regions <= preg->region_count)
		return SBI_ENOSPC;

	/* Read "base" DT property */
	val = fdt_getprop(fdt, region_offset, "base", &len);
	if (!val || len != 8)
		return SBI_EINVAL;
	val64 = fdt32_to_cpu(val[0]);
	val64 = (val64 << 32) | fdt32_to_cpu(val[1]);
	base = val64;

	/* Read "order" DT property */
	val = fdt_getprop(fdt, region_offset, "order", &len);
	if (!val || len != 4)
		return SBI_EINVAL;
	val32 = fdt32_to_cpu(*val);
	if (val32 < 3 || __riscv_xlen < val32)
		return SBI_EINVAL;
	order = val32;

	flags = region_access & (SBI_DOMAIN_MEMREGION_ACCESS_MASK
				| SBI_DOMAIN_MEMREGION_ENF_PERMISSIONS);

	/* Read "mmio" DT property */
	if (fdt_get_property(fdt, region_offset, "mmio", NULL))
		flags |= SBI_DOMAIN_MEMREGION_MMIO;

	sbi_domain_memregion_init(base, (order == __riscv_xlen) ? ~0UL : BIT(order),
				  flags, &preg->dom->regions[preg->region_count]);

	preg->region_count++;

	return 0;
}

static int __fdt_parse_domain(const void *fdt, int domain_offset, void *opaque)
{
	u32 val32;
	u64 val64;
	const u32 *val;
	struct sbi_domain *dom;
	struct sbi_hartmask *mask;
	struct sbi_hartmask assign_mask;
	struct parse_region_data preg;
	int *cold_domain_offset = opaque;
	struct sbi_domain_memregion *reg;
	int i, err = 0, len, cpus_offset, cpu_offset, doffset;

	dom = sbi_zalloc(sizeof(*dom));
	if (!dom)
		return SBI_ENOMEM;

	dom->regions = sbi_calloc(sizeof(*dom->regions),
				  FDT_DOMAIN_REGION_MAX_COUNT + 1);
	if (!dom->regions) {
		err = SBI_ENOMEM;
		goto fail_free_domain;
	}
	preg.dom = dom;
	preg.region_count = 0;
	preg.max_regions = FDT_DOMAIN_REGION_MAX_COUNT;

	mask = sbi_zalloc(sizeof(*mask));
	if (!mask) {
		err = SBI_ENOMEM;
		goto fail_free_regions;
	}

	/* Read DT node name */
	strncpy(dom->name, fdt_get_name(fdt, domain_offset, NULL),
		    sizeof(dom->name));
	dom->name[sizeof(dom->name) - 1] = '\0';

	/* Setup possible HARTs mask */
	SBI_HARTMASK_INIT(mask);
	dom->possible_harts = mask;
	val = fdt_getprop(fdt, domain_offset, "possible-harts", &len);
	len = len / sizeof(u32);
	if (val && len) {
		for (i = 0; i < len; i++) {
			cpu_offset = fdt_node_offset_by_phandle(fdt,
							fdt32_to_cpu(val[i]));
			if (cpu_offset < 0) {
				err = cpu_offset;
				goto fail_free_all;
			}

			err = fdt_parse_hart_id(fdt, cpu_offset, &val32);
			if (err)
				goto fail_free_all;

			if (!fdt_node_is_enabled(fdt, cpu_offset))
				continue;

			sbi_hartmask_set_hartid(val32, mask);
		}
	}

	/* Setup memregions from DT */
	err = fdt_iterate_each_memregion_ro(fdt, domain_offset, &preg,
					    __fdt_parse_region);
	if (err)
		goto fail_free_all;

	/*
	 * Copy over root domain memregions which don't allow
	 * read, write and execute from lower privilege modes.
	 *
	 * These root domain memregions without read, write,
	 * and execute permissions include:
	 * 1) firmware region protecting the firmware memory
	 * 2) mmio regions protecting M-mode only mmio devices
	 */
	sbi_domain_for_each_memregion(&root, reg) {
		if ((reg->flags & SBI_DOMAIN_MEMREGION_SU_READABLE) ||
		    (reg->flags & SBI_DOMAIN_MEMREGION_SU_WRITABLE) ||
		    (reg->flags & SBI_DOMAIN_MEMREGION_SU_EXECUTABLE))
			continue;
		if (preg.max_regions <= preg.region_count) {
			err = SBI_EINVAL;
			goto fail_free_all;
		}
		memcpy(&dom->regions[preg.region_count++], reg, sizeof(*reg));
	}
	dom->fw_region_inited = root.fw_region_inited;

	/* Read "boot-hart" DT property */
	val32 = -1U;
	val = fdt_getprop(fdt, domain_offset, "boot-hart", &len);
	if (val && len >= 4) {
		cpu_offset = fdt_node_offset_by_phandle(fdt,
							 fdt32_to_cpu(*val));
		if (cpu_offset >= 0 && fdt_node_is_enabled(fdt, cpu_offset))
			fdt_parse_hart_id(fdt, cpu_offset, &val32);
	} else {
		if (domain_offset == *cold_domain_offset)
			val32 = current_hartid();
	}
	dom->boot_hartid = val32;

	/* Read "next-arg1" DT property */
	val64 = 0;
	val = fdt_getprop(fdt, domain_offset, "next-arg1", &len);
	if (val && len >= 8) {
		val64 = fdt32_to_cpu(val[0]);
		val64 = (val64 << 32) | fdt32_to_cpu(val[1]);
	} else {
		val64 = sbi_scratch_thishart_ptr()->next_arg1;
	}
	dom->next_arg1 = val64;

	/* Read "next-addr" DT property */
	val64 = 0;
	val = fdt_getprop(fdt, domain_offset, "next-addr", &len);
	if (val && len >= 8) {
		val64 = fdt32_to_cpu(val[0]);
		val64 = (val64 << 32) | fdt32_to_cpu(val[1]);
	} else {
		if (domain_offset == *cold_domain_offset)
			val64 = sbi_scratch_thishart_ptr()->next_addr;
	}
	dom->next_addr = val64;

	/* Read "next-mode" DT property */
	val32 = 0x1;
	val = fdt_getprop(fdt, domain_offset, "next-mode", &len);
	if (val && len >= 4) {
		val32 = fdt32_to_cpu(val[0]);
		if (val32 != 0x0 && val32 != 0x1)
			val32 = 0x1;
	} else {
		if (domain_offset == *cold_domain_offset)
			val32 = sbi_scratch_thishart_ptr()->next_mode;
	}
	dom->next_mode = val32;

	/* Read "system-reset-allowed" DT property */
	if (fdt_get_property(fdt, domain_offset,
			     "system-reset-allowed", NULL))
		dom->system_reset_allowed = true;
	else
		dom->system_reset_allowed = false;

	/* Read "system-suspend-allowed" DT property */
	if (fdt_get_property(fdt, domain_offset,
			     "system-suspend-allowed", NULL))
		dom->system_suspend_allowed = true;
	else
		dom->system_suspend_allowed = false;

	/* Find /cpus DT node */
	cpus_offset = fdt_path_offset(fdt, "/cpus");
	if (cpus_offset < 0) {
		err = cpus_offset;
		goto fail_free_all;
	}

	/* HART to domain assignment mask based on CPU DT nodes */
	sbi_hartmask_clear_all(&assign_mask);
	fdt_for_each_subnode(cpu_offset, fdt, cpus_offset) {
		err = fdt_parse_hart_id(fdt, cpu_offset, &val32);
		if (err)
			continue;

		if (SBI_HARTMASK_MAX_BITS <= sbi_hartid_to_hartindex(val32))
			continue;

		if (!fdt_node_is_enabled(fdt, cpu_offset))
			continue;

		/* This is an optional property */
		val = fdt_getprop(fdt, cpu_offset, "opensbi-domain", &len);
		if (!val || len < 4)
			continue;

		/* However, it should be valid if specified */
		doffset = fdt_node_offset_by_phandle(fdt, fdt32_to_cpu(*val));
		if (doffset < 0) {
			err = doffset;
			goto fail_free_all;
		}

		if (doffset == domain_offset)
			sbi_hartmask_set_hartid(val32, &assign_mask);
	}

	/* Register the domain */
	err = sbi_domain_register(dom, &assign_mask);
	if (err)
		goto fail_free_all;

	return 0;

fail_free_all:
	sbi_free(mask);
fail_free_regions:
	sbi_free(dom->regions);
fail_free_domain:
	sbi_free(dom);
	return err;
}

int fdt_domains_populate(const void *fdt)
{
	const u32 *val;
	int cold_domain_offset;
	u32 hartid, cold_hartid;
	int err, len, cpus_offset, cpu_offset;

	/* Sanity checks */
	if (!fdt)
		return SBI_EINVAL;

	/* Find /cpus DT node */
	cpus_offset = fdt_path_offset(fdt, "/cpus");
	if (cpus_offset < 0)
		return cpus_offset;

	/* Find coldboot HART domain DT node offset */
	cold_domain_offset = -1;
	cold_hartid = current_hartid();
	fdt_for_each_subnode(cpu_offset, fdt, cpus_offset) {
		err = fdt_parse_hart_id(fdt, cpu_offset, &hartid);
		if (err)
			continue;

		if (hartid != cold_hartid)
			continue;

		if (!fdt_node_is_enabled(fdt, cpu_offset))
			continue;

		val = fdt_getprop(fdt, cpu_offset, "opensbi-domain", &len);
		if (val && len >= 4)
			cold_domain_offset = fdt_node_offset_by_phandle(fdt,
							   fdt32_to_cpu(*val));

		break;
	}

	/* Iterate over each domain in FDT and populate details */
	return fdt_iterate_each_domain_ro(fdt, &cold_domain_offset,
					  __fdt_parse_domain);
}
