/*
 * (C) Copyright 2017 Rockchip Electronics Co., Ltd
 *
 * SPDX-License-Identifier:     GPL-2.0+
 */

#include <common.h>
#include <bootm.h>
#include <linux/list.h>
#include <libfdt.h>
#include <malloc.h>
#include <asm/arch/resource_img.h>
#include <asm/arch/rockchip_crc.h>
#include <boot_rkimg.h>
#include <asm/arch/boot_mode.h>
#include <asm/io.h>

#define TAG_KERNEL			0x4C4E524B

#define DTB_FILE			"rk-kernel.dtb"

#define BOOTLOADER_MESSAGE_OFFSET_IN_MISC	(16 * 1024)
#define BOOTLOADER_MESSAGE_BLK_OFFSET		(BOOTLOADER_MESSAGE_OFFSET_IN_MISC >> 9)
DECLARE_GLOBAL_DATA_PTR;

struct bootloader_message {
	char command[32];
	char status[32];
	char recovery[768];

	/*
         * The 'recovery' field used to be 1024 bytes.  It has only ever
	 * been used to store the recovery command line, so 768 bytes
	 * should be plenty.  We carve off the last 256 bytes to store the
	 * stage string (for multistage packages) and possible future
	 * expansion.
         */
	char stage[32];
	char slot_suffix[32];
	char reserved[192];
};

struct rockchip_image {
	uint32_t tag;
	uint32_t size;
	int8_t image[1];
	uint32_t crc;
};

#if !defined(CONFIG_ARM64)
#ifdef CONFIG_LMB
static void boot_start_lmb(bootm_headers_t *images)
{
	ulong		mem_start;
	phys_size_t	mem_size;

	lmb_init(&images->lmb);

	mem_start = env_get_bootm_low();
	mem_size = env_get_bootm_size();

	lmb_add(&images->lmb, (phys_addr_t)mem_start, mem_size);

	arch_lmb_reserve(&images->lmb);
	board_lmb_reserve(&images->lmb);
}
#else
static inline void boot_start_lmb(bootm_headers_t *images) { }
#endif

static void boot_lmb_init(bootm_headers_t *images)
{
	boot_start_lmb(images);
	images->state = BOOTM_STATE_OS_GO;
}
#endif

/*
 * non-OTA packaged kernel.img & boot.img
 * return the image size on success, and a
 * negative value on error.
 */
static int read_rockchip_image(struct blk_desc *dev_desc,
			       disk_partition_t *part_info,
			       void *dst)
{
	struct rockchip_image *img;
	int header_len = 8;
	int cnt;
	int ret;
#ifdef CONFIG_ROCKCHIP_CRC
	u32 crc32;
#endif

	img = memalign(ARCH_DMA_MINALIGN, RK_BLK_SIZE);
	if (!img) {
		printf("out of memory\n");
		return -ENOMEM;
	}

	/* read first block with header imformation */
	ret = blk_dread(dev_desc, part_info->start, 1, img);
	if (ret != 1) {
		ret = -EIO;
		goto err;
	}

	if (img->tag != TAG_KERNEL) {
		printf("%s: invalid image tag(0x%x)\n", part_info->name, img->tag);
		ret = -EINVAL;
		goto err;
	}

	memcpy(dst, img->image, RK_BLK_SIZE - header_len);
	/*
	 * read the rest blks
	 * total size  = image size + 8 bytes header + 4 bytes crc32
	 */
	cnt = DIV_ROUND_UP(img->size + 8 + 4, RK_BLK_SIZE);
	ret = blk_dread(dev_desc, part_info->start + 1, cnt - 1,
			dst + RK_BLK_SIZE - header_len);
	if (ret != (cnt - 1)) {
		printf("%s try to read %d blocks failed, only read %d blocks\n",
		       part_info->name, cnt - 1, ret);
		ret = -EIO;
	} else {
		ret = img->size;
	}

#ifdef CONFIG_ROCKCHIP_CRC
	printf("%s image CRC32 verify... ", part_info->name);
	crc32 = rockchip_crc_verify((unsigned char *)(unsigned long)dst,
				  img->size + 4);
	if (!crc32) {
		printf("fail!\n");
		ret = -EINVAL;
	} else {
		printf("okay.\n");
	}
#endif

err:
	free(img);
	return ret;
}

/* Gets the storage type of the current device */
int get_bootdev_type(void)
{
	int type = 0;
	char *boot_media = NULL;
	char boot_options[128] = {0};
	static int appended;

	#ifdef CONFIG_EMMC_BOOT
		type = IF_TYPE_MMC;
		boot_media = "emmc";
	#endif /* CONFIG_EMMC_BOOT */
	#ifdef CONFIG_QSPI_BOOT
		type = IF_TYPE_SPI_NAND;
		boot_media = "nand";
	#endif /* CONFIG_QSPI_BOOT */
	#ifdef CONFIG_NAND_BOOT
		type = IF_TYPE_RKNAND;
		boot_media = "nand";
	#endif /* CONFIG_NAND_BOOT */
	#ifdef CONFIG_NOR_BOOT
		type = IF_TYPE_SPI_NOR;
	#endif /* CONFIG_NOR_BOOT */

	/* For current use(Only EMMC support!) */
	if (!type) {
		type = IF_TYPE_MMC;
		boot_media = "emmc";
	}

	if (!appended && boot_media) {
		appended = 1;
		/*
		 * 1. androidboot.mode=charger has higher priority, not override;
		 * 2. rknand doesn't need "androidboot.mode=";
		 */
		if (env_exist("bootargs", "androidboot.mode=charger") ||
		    (type == IF_TYPE_RKNAND))
			snprintf(boot_options, sizeof(boot_options),
				 "storagemedia=%s", boot_media);
		else
			snprintf(boot_options, sizeof(boot_options),
				 "storagemedia=%s androidboot.mode=%s",
				 boot_media, boot_media);
		env_update("bootargs", boot_options);
	}

	return type;
}

struct blk_desc *rockchip_get_bootdev(void)
{
	struct blk_desc *dev_desc;
	int dev_type;

	dev_type = get_bootdev_type();
	dev_desc = blk_get_devnum_by_type(dev_type, 0);

	return dev_desc;
}

static int boot_mode = -1;
int rockchip_get_boot_mode(void)
{
	struct blk_desc *dev_desc;
	disk_partition_t part_info;
	struct bootloader_message *bmsg;
	int size = DIV_ROUND_UP(sizeof(struct bootloader_message), RK_BLK_SIZE)
		   * RK_BLK_SIZE;
	int ret;
	uint32_t reg_boot_mode;

	if (boot_mode != -1)
		return boot_mode;

	dev_desc = rockchip_get_bootdev();
	ret = part_get_info_by_name(dev_desc, PART_MISC,
			&part_info);
	if (ret < 0) {
		printf("get part %s fail %d\n", PART_MISC, ret);
		return -EIO;
	}

	bmsg = memalign(ARCH_DMA_MINALIGN, size);
	ret = blk_dread(dev_desc,
			part_info.start + BOOTLOADER_MESSAGE_BLK_OFFSET,
			size >> 9, bmsg);
	if (ret != (size >> 9)) {
		free(bmsg);
		return -EIO;
	}

	/* Mode from misc partition */
	if (!strcmp(bmsg->command, "boot-recovery")) {
		boot_mode = BOOT_MODE_RECOVERY;
	} else {
		/* Mode from boot mode register */
		reg_boot_mode = readl((void *)CONFIG_ROCKCHIP_BOOT_MODE_REG);
		writel(BOOT_NORMAL, (void *)CONFIG_ROCKCHIP_BOOT_MODE_REG);

		switch (reg_boot_mode) {
		case BOOT_NORMAL:
			printf("boot mode: normal\n");
			boot_mode = BOOT_MODE_NORMAL;
			break;
		case BOOT_FASTBOOT:
			printf("boot mode: bootloader\n");
			boot_mode = BOOT_MODE_BOOTLOADER;
			break;
		case BOOT_LOADER:
			printf("boot mode: loader\n");
			boot_mode = BOOT_MODE_LOADER;
			break;
		case BOOT_RECOVERY:
			/* printf("boot mode: recovery\n"); */
			boot_mode = BOOT_MODE_RECOVERY;
			break;
		case BOOT_UMS:
			printf("boot mode: ums\n");
			boot_mode = BOOT_MODE_UMS;
			break;
		case BOOT_CHARGING:
			printf("boot mode: charging\n");
			boot_mode = BOOT_MODE_CHARGING;
			break;
		default:
			printf("boot mode: None\n");
			boot_mode = BOOT_MODE_UNDEFINE;
		}
	}

	return boot_mode;
}

int boot_rockchip_image(struct blk_desc *dev_desc, disk_partition_t *boot_part)
{
	ulong fdt_addr_r = env_get_ulong("fdt_addr_r", 16, 0);
	ulong ramdisk_addr_r = env_get_ulong("ramdisk_addr_r", 16, 0);
	ulong kernel_addr_r = env_get_ulong("kernel_addr_r", 16, 0x480000);
	disk_partition_t kernel_part;
	int ramdisk_size;
	int kernel_size;
	int fdt_size;
	int ret = 0;
	int part_num;

	printf("=Booting Rockchip format image=\n");
	part_num = part_get_info_by_name(dev_desc, PART_KERNEL,
					 &kernel_part);

	if (part_num < 0 || !boot_part) {
		printf("%s kernel or boot part info error\n", __func__);
		ret = -EINVAL;
		goto out;
	}

	kernel_size = read_rockchip_image(dev_desc, &kernel_part,
					  (void *)kernel_addr_r);
	if (kernel_size < 0) {
		printf("%s kernel part read error\n", __func__);
		ret = -EINVAL;
		goto out;
	}

	ramdisk_size = read_rockchip_image(dev_desc, boot_part,
					   (void *)ramdisk_addr_r);
	if (ramdisk_size < 0) {
		printf("%s ramdisk part %s read error\n", __func__,
		       boot_part->name);
		ramdisk_size = 0;
	}

	if (gd->fdt_blob != (void *)fdt_addr_r) {
		fdt_size = rockchip_read_resource_file((void *)fdt_addr_r,
						       DTB_FILE, 0, 0);
		if (fdt_size < 0) {
			printf("%s fdt read error\n", __func__);
			ret = -EINVAL;
			goto out;
		}
	}

	printf("kernel   @ 0x%08lx (0x%08x)\n", kernel_addr_r, kernel_size);
	printf("ramdisk  @ 0x%08lx (0x%08x)\n", ramdisk_addr_r, ramdisk_size);
#if defined(CONFIG_ARM64)
	char cmdbuf[64];
	sprintf(cmdbuf, "booti 0x%lx 0x%lx:0x%x 0x%lx",
		kernel_addr_r, ramdisk_addr_r, ramdisk_size, fdt_addr_r);
	run_command(cmdbuf, 0);
#else
	boot_lmb_init(&images);
	images.ep = kernel_addr_r;
	images.initrd_start = ramdisk_addr_r;
	images.initrd_end = ramdisk_addr_r + ramdisk_size;
	images.ft_addr = (void *)fdt_addr_r;
	images.ft_len = fdt_totalsize(fdt_addr_r);
	do_bootm_linux(0, 0, NULL, &images);
#endif
out:
	return ret;
}
