/*
 * Copyright 2013 Google Inc.
 * Copyright 2018 Facebook, Inc.
 * Copyright 2019 9elements Agency GmbH <patrick.rudolph@9elements.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of
 * the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 */

#include <console/console.h>
#include <bootmem.h>
#include <stdlib.h>
#include <program_loading.h>
#include <string.h>
#include <commonlib/compression.h>
#include <commonlib/cbfs_serialized.h>
#include <lib.h>
#include <fit.h>
#include <endian.h>

/* Implements a Berkley Boot Loader (BBL) compatible payload loading */

#define MAX_KERNEL_SIZE (64*MiB)

#if CONFIG(ARCH_RISCV_RV32)
#define SECTION_ALIGN (4 * MiB)
#endif
#if CONFIG(ARCH_RISCV_RV64)
#define SECTION_ALIGN (2 * MiB)
#endif

static size_t get_kernel_size(const struct fit_image_node *node)
{
	/*
	 * Since we don't have a way to determine the uncompressed size of the
	 * kernel, we have to keep as much memory as possible free for use by
	 * the kernel immediately after the end of the kernel image. The amount
	 * of space required will vary depending on selected features, and is
	 * effectively unbound.
	 */

	printk(BIOS_INFO,
	       "FIT: Leaving additional %u MiB of free space after kernel.\n",
	       MAX_KERNEL_SIZE >> 20);

	return node->size + MAX_KERNEL_SIZE;
}

/**
 * Place the region in free memory range.
 *
 * The caller has to set region->offset to the minimum allowed address.
 */
static bool fit_place_mem(const struct range_entry *r, void *arg)
{
	struct region *region = arg;
	resource_t start;

	if (range_entry_tag(r) != BM_MEM_RAM)
		return true;

	/* Section must be aligned at page boundary */
	start = ALIGN_UP(MAX(region->offset, range_entry_base(r)), SECTION_ALIGN);

	if (start + region->size < range_entry_end(r)) {
		region->offset = (size_t)start;
		return false;
	}

	return true;
}

bool fit_payload_arch(struct prog *payload, struct fit_config_node *config,
		      struct region *kernel,
		      struct region *fdt,
		      struct region *initrd)
{
	void *arg = NULL;

	if (!config->fdt || !fdt) {
		printk(BIOS_CRIT, "CRIT: Providing a valid FDT is mandatory to "
		       "boot a RISC-V kernel!\n");
		return false;
		/* TODO: Fall back to the ROM FDT? */
	}

	/* Update kernel size from image header, if possible */
	kernel->size = get_kernel_size(config->kernel);
	printk(BIOS_DEBUG, "FIT: Using kernel size of 0x%zx bytes\n",
	       kernel->size);

	/*
	 * The code assumes that bootmem_walk provides a sorted list of memory
	 * regions, starting from the lowest address.
	 * The order of the calls here doesn't matter, as the placement is
	 * enforced in the called functions.
	 * For details check code on top.
	 */
	kernel->offset = 0;
	if (!bootmem_walk(fit_place_mem, kernel))
		return false;

	/* Mark as reserved for future allocations. */
	bootmem_add_range(kernel->offset, kernel->size, BM_MEM_PAYLOAD);

	/* Place FDT and INITRD after kernel. */

	/* Place INITRD */
	if (config->ramdisk) {
		initrd->offset = kernel->offset + kernel->size;

		if (!bootmem_walk(fit_place_mem, initrd))
			return false;
		/* Mark as reserved for future allocations. */
		bootmem_add_range(initrd->offset, initrd->size, BM_MEM_PAYLOAD);
	}

	/* Place FDT */
	fdt->offset = kernel->offset + kernel->size;

	if (!bootmem_walk(fit_place_mem, fdt))
		return false;
	/* Mark as reserved for future allocations. */
	bootmem_add_range(fdt->offset, fdt->size, BM_MEM_PAYLOAD);

	/* Kernel expects FDT as argument */
	arg = (void *)fdt->offset;

	prog_set_entry(payload, (void *)kernel->offset, arg);

	bootmem_dump_ranges();

	return true;
}