/*
 * A simple tool to generate bootable image for sunxi platform.
 *
 * Copyright (C) 2007-2011 Allwinner Technology Co., Ltd.
 *	Tom Cubie <tangliang@allwinnertech.com>
 * Copyright (C) 2014 Alexandru Gagniuc <mr.nuke.me@gmail.com>
 * Subject to the GNU GPL v2, or (at your option) any later version.
 */

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>

/* boot head definition from sun4i boot code */
struct boot_file_head {
	uint32_t jump_instruction;	/* one intruction jumping to real code */
	uint8_t magic[8];		/* ="eGON.BT0" or "eGON.BT1", not C-style str */
	uint32_t check_sum;		/* generated by PC */
	uint32_t length;		/* generated by PC */
	/* We use a simplified header, only filling in what is needed by the
	 * boot ROM. To be compatible with Allwinner tools the larger header
	 * below should be used, followed by a custom header if desired. */
	uint8_t pad[12];		/* align to 32 bytes */
};

static const char *BOOT0_MAGIC = "eGON.BT0";
static const uint32_t STAMP_VALUE = 0x5F0A6C39;
static const int HEADER_SIZE = 32;
/* Checksum at most 24 KiB */
#define SRAM_LOAD_MAX_SIZE ((24 << 10) - sizeof(struct boot_file_head))
static const int BLOCK_SIZE = 512;

inline static uint32_t le32_to_h(const void *src)
{
	const uint8_t *b = src;
	return ((b[3] << 24) | (b[2] << 16) | (b[1] << 8) | (b[0] << 0));
}

inline static void h_to_le32(uint32_t val32, void *dest)
{
	uint8_t *b = dest;
	b[0] = (val32 >> 0) & 0xff;
	b[1] = (val32 >> 8) & 0xff;
	b[2] = (val32 >> 16) & 0xff;
	b[3] = (val32 >> 24) & 0xff;
};

static void serialize_header(void *dest, const struct boot_file_head *hdr)
{
	/* Unused fields are zero */
	memset(dest, 0, HEADER_SIZE);

	h_to_le32(hdr->jump_instruction, dest + 0);
	memcpy(dest + 4, BOOT0_MAGIC, 8);
	h_to_le32(hdr->check_sum, dest + 12);
	h_to_le32(hdr->length, dest + 16);
}

/* Check sum function from sun4i boot code */
static int fill_check_sum(struct boot_file_head *hdr, const void *boot_code)
{
	size_t i;
	uint8_t raw_hdr[HEADER_SIZE];
	uint32_t chksum;

	if ((hdr->length & 0x3) != 0) {
		fprintf(stderr, "BUG! Load size is not 4-byte aligned\n");
		return EXIT_FAILURE;
	}

	/* Fill in checksum seed */
	hdr->check_sum = STAMP_VALUE;

	chksum = 0;
	/* Checksum the header */
	serialize_header(raw_hdr, hdr);
	for (i = 0; i < HEADER_SIZE; i += 4)
		chksum += le32_to_h(raw_hdr + i);

	/* Checksum the boot code */
	for (i = 0; i < hdr->length - HEADER_SIZE; i += 4)
		chksum += le32_to_h(boot_code + i);

	/* write back check sum */
	hdr->check_sum = chksum;

	return EXIT_SUCCESS;
}

static uint32_t align(uint32_t size, uint32_t alignment)
{
	return ((size + alignment - 1) / alignment) * alignment;
}

static void fill_header(struct boot_file_head *hdr, size_t load_size)
{
	/* B instruction */
	hdr->jump_instruction = 0xEA000000;
	/* Jump to the first instr after the header */
	hdr->jump_instruction |= (sizeof(*hdr) / sizeof(uint32_t) - 2);
	/* No '0' termination in magic string */
	memcpy(&hdr->magic, BOOT0_MAGIC, 8);

	hdr->length = align(load_size + sizeof(hdr), BLOCK_SIZE);
}

static long int fsize(FILE *file)
{
	struct stat s;
	int fd = fileno(file);
	if (fd == -1) return -1;
	if (fstat(fd, &s) == -1) return -1;
	return s.st_size;
}

int main(int argc, char *argv[])
{
	FILE *fd_in, *fd_out;
	struct boot_file_head hdr;
	long int file_size, load_size;
	void *file_data;
	uint8_t raw_hdr[HEADER_SIZE];
	int count;

	/*
	 * TODO: We could take an additional argument to see how much of the
	 * file to checksum. This way, the build system can tell us how large
	 * the bootblock is, so we can tell the BROM to load only the bootblock.
	 */
	if (argc < 2) {
		printf("\tThis program makes an input bin file to sun4i "
		       "bootable image.\n"
		       "\tUsage: %s input_file out_putfile\n", argv[0]);
		return EXIT_FAILURE;
	}

	fd_in = fopen(argv[1], "rb");
	if (!fd_in) {
		fprintf(stderr, "Cannot open input %s", argv[1]);
		return EXIT_FAILURE;
	}

	/* Get input file size */
	file_size = fsize(fd_in);
	if (file_size == -1) {
		fprintf(stderr, "can't determine file size\n");
		return EXIT_FAILURE;
	}
	if ((file_data = malloc(file_size)) == NULL) {
		fprintf(stderr, "Cannot allocate memory\n");
		return EXIT_FAILURE;
	}

	printf("File size: 0x%x\n", file_size);
	if (fread(file_data, file_size, 1, fd_in) != 1) {
		fprintf(stderr, "Cannot read %s: %s\n", argv[1],
			strerror(errno));
		return EXIT_FAILURE;
	}

	load_size = align(file_size, sizeof(uint32_t));

	if (load_size > SRAM_LOAD_MAX_SIZE)
		load_size = SRAM_LOAD_MAX_SIZE;

	printf("Load size: 0x%x\n", load_size);

	fd_out = fopen(argv[2], "w");
	if (!fd_out) {
		fprintf(stderr, "Cannot open output %s\n", argv[2]);
		return EXIT_FAILURE;
	}

	/* Fill the header */
	fill_header(&hdr, load_size);
	fill_check_sum(&hdr, file_data);

	/* Now write the header */
	serialize_header(raw_hdr, &hdr);
	if (fwrite(raw_hdr, HEADER_SIZE, 1, fd_out) != 1) {
		fprintf(stderr, "Cannot write header to %s: %s\n", argv[1],
			strerror(errno));
		return EXIT_FAILURE;
	}

	/* And finally, the boot code */
	if (fwrite(file_data, file_size, 1, fd_out) != 1) {
		fprintf(stderr, "Cannot write to %s: %s\n", argv[1],
			strerror(errno));
		return EXIT_FAILURE;
	}

	fclose(fd_in);
	fclose(fd_out);

	return EXIT_SUCCESS;
}
