/* ELF Boot loader
 * As we have seek, this implementation can be straightforward.
 * 2003-07 by SONE Takeshi
 */

#include "config.h"
#include "kernel/kernel.h"
#include "libc/diskio.h"
#include "arch/common/elf_boot.h"
#include "libopenbios/elf_load.h"
#include "libopenbios/sys_info.h"
#include "libopenbios/ipchecksum.h"
#include "libopenbios/bindings.h"
#include "libopenbios/ofmem.h"
#define printf printk
#define debug printk

#define DEBUG		0

#define MAX_HEADERS	0x20
#define BS		0x100	/* smallest step used when looking for the ELF header */

#ifdef CONFIG_PPC
extern void             flush_icache_range( char *start, char *stop );
#endif

/* FreeBSD and possibly others mask the high 8 bits */
#define addr_fixup(addr) ((addr) & 0x00ffffff)

static char *image_name, *image_version;
static int fd;

/* Note: avoid name collision with platforms which have their own version of calloc() */
static void *ob_calloc(size_t nmemb, size_t size)
{
    size_t alloc_size = nmemb * size;
    void *mem;

    if (alloc_size < nmemb || alloc_size < size) {
        printf("calloc overflow: %u, %u\n", nmemb, size);
        return NULL;
    }

    mem = malloc(alloc_size);
    memset(mem, 0, alloc_size);

    return mem;
}

static int check_mem_ranges(struct sys_info *info,
	Elf_phdr *phdr, int phnum)
{
    int i, j;
    unsigned long start, end;
    unsigned long prog_start, prog_end;
    struct memrange *mem;

    prog_start = virt_to_phys(&_start);
    prog_end = virt_to_phys(&_end);

    for (i = 0; i < phnum; i++) {
	if (phdr[i].p_type != PT_LOAD)
	    continue;
	start = addr_fixup(phdr[i].p_paddr);
	end = start + phdr[i].p_memsz;
	if (start < prog_start && end > prog_start)
	    goto conflict;
	if (start < prog_end && end > prog_end)
	    goto conflict;
	mem=info->memrange;
	for (j = 0; j < info->n_memranges; j++) {
	    if (mem[j].base <= start && mem[j].base + mem[j].size >= end)
		break;
	}
	if (j >= info->n_memranges)
	    goto badseg;
    }
    return 1;

conflict:
    printf("%s occupies [%#lx-%#lx]\n", program_name, prog_start, prog_end);

badseg:
    printf("Segment %d [%#lx-%#lx] doesn't fit into memory\n", i, start, end-1);
    return 0;
}

static unsigned long process_image_notes(Elf_phdr *phdr, int phnum,
                                         unsigned short *sum_ptr,
                                         unsigned int offset)
{
    int i;
    char *buf = NULL;
    int retval = 0;
    unsigned long addr, end;
    Elf_Nhdr *nhdr;
    const char *name;
    void *desc;

    for (i = 0; i < phnum; i++) {
	if (phdr[i].p_type != PT_NOTE)
	    continue;
	buf = malloc(phdr[i].p_filesz);
	seek_io(fd, offset + phdr[i].p_offset);
	if ((size_t)read_io(fd, buf, phdr[i].p_filesz) != phdr[i].p_filesz) {
	    printf("Can't read note segment\n");
	    goto out;
	}
	addr = (unsigned long) buf;
	end = addr + phdr[i].p_filesz;
	while (addr < end) {
	    nhdr = (Elf_Nhdr *) addr;
	    addr += sizeof(Elf_Nhdr);
	    name = (const char *) addr;
	    addr += (nhdr->n_namesz+3) & ~3;
	    desc = (void *) addr;
	    addr += (nhdr->n_descsz+3) & ~3;

	    if (nhdr->n_namesz==sizeof(ELF_NOTE_BOOT)
		    && memcmp(name, ELF_NOTE_BOOT, sizeof(ELF_NOTE_BOOT))==0) {
		if (nhdr->n_type == EIN_PROGRAM_NAME) {
		    image_name = ob_calloc(1, nhdr->n_descsz + 1);
		    memcpy(image_name, desc, nhdr->n_descsz);
		}
		if (nhdr->n_type == EIN_PROGRAM_VERSION) {
		    image_version = ob_calloc(1, nhdr->n_descsz + 1);
		    memcpy(image_version, desc, nhdr->n_descsz);
		}
		if (nhdr->n_type == EIN_PROGRAM_CHECKSUM) {
		    *sum_ptr = *(unsigned short *) desc;
		    debug("Image checksum: %#04x\n", *sum_ptr);
		    /* Where in the file */
		    retval = phdr[i].p_offset
			+ (unsigned long) desc - (unsigned long) buf;
		}
	    }
	}
    }
out:
    close_io(fd);
    if (buf)
	free(buf);
    return retval;
}

static int load_segments(Elf_phdr *phdr, int phnum,
                         unsigned long checksum_offset,
                         unsigned int offset, unsigned long *bytes)
{
    //unsigned int start_time, time;
    int i;

    *bytes = 0;
    // start_time = currticks();
    for (i = 0; i < phnum; i++) {
	if (phdr[i].p_type != PT_LOAD)
	    continue;
	debug("segment %d addr:" FMT_elf " file:" FMT_elf " mem:" FMT_elf " ",
              i, addr_fixup(phdr[i].p_paddr), phdr[i].p_filesz, phdr[i].p_memsz);
	seek_io(fd, offset + phdr[i].p_offset);
	debug("loading... ");
	if ((size_t)read_io(fd, phys_to_virt(addr_fixup(phdr[i].p_paddr)), phdr[i].p_filesz)
		!= phdr[i].p_filesz) {
	    printf("Can't read program segment %d\n", i);
	    return 0;
	}
	bytes += phdr[i].p_filesz;
	debug("clearing... ");
	memset(phys_to_virt(addr_fixup(phdr[i].p_paddr) + phdr[i].p_filesz), 0,
		phdr[i].p_memsz - phdr[i].p_filesz);
	if (phdr[i].p_offset <= checksum_offset
		&& phdr[i].p_offset + phdr[i].p_filesz >= checksum_offset+2) {
	    debug("clearing checksum... ");
	    memset(phys_to_virt(addr_fixup(phdr[i].p_paddr) + checksum_offset
			- phdr[i].p_offset), 0, 2);
	}
	debug("ok\n");

    }
    // time = currticks() - start_time;
    //debug("Loaded %lu bytes in %ums (%luKB/s)\n", bytes, time,
    //	    time? bytes/time : 0);
    debug("Loaded %lu bytes \n", *bytes);

    return 1;
}

static int verify_image(Elf_ehdr *ehdr, Elf_phdr *phdr, int phnum,
	unsigned short image_sum)
{
    unsigned short sum, part_sum;
    unsigned long offset;
    int i;

    sum = 0;
    offset = 0;

    part_sum = ipchksum(ehdr, sizeof *ehdr);
    sum = add_ipchksums(offset, sum, part_sum);
    offset += sizeof *ehdr;

    part_sum = ipchksum(phdr, phnum * sizeof(*phdr));
    sum = add_ipchksums(offset, sum, part_sum);
    offset += phnum * sizeof(*phdr);

    for (i = 0; i < phnum; i++) {
	if (phdr[i].p_type != PT_LOAD)
	    continue;
	part_sum = ipchksum(phys_to_virt(addr_fixup(phdr[i].p_paddr)), phdr[i].p_memsz);
	sum = add_ipchksums(offset, sum, part_sum);
	offset += phdr[i].p_memsz;
    }

    if (sum != image_sum) {
	printf("Verify FAILED (image:%#04x vs computed:%#04x)\n",
		image_sum, sum);
	return 0;
    }
    return 1;
}

static inline unsigned padded(unsigned s)
{
    return (s + 3) & ~3;
}

static Elf_Bhdr *add_boot_note(Elf_Bhdr *bhdr, const char *name,
	unsigned type, const char *desc, unsigned descsz)
{
    Elf_Nhdr nhdr;
    unsigned ent_size, new_size, pad;
    char *addr;

    if (!bhdr)
	return NULL;

    nhdr.n_namesz = name? strlen(name)+1 : 0;
    nhdr.n_descsz = descsz;
    nhdr.n_type = type;
    ent_size = sizeof(nhdr) + padded(nhdr.n_namesz) + padded(nhdr.n_descsz);
    if (bhdr->b_size + ent_size > 0xffff) {
	printf("Boot notes too big\n");
	free(bhdr);
	return NULL;
    }
    if (bhdr->b_size + ent_size > bhdr->b_checksum) {
	do {
	    new_size = bhdr->b_checksum * 2;
	} while (new_size < bhdr->b_size + ent_size);
	if (new_size > 0xffff)
	    new_size = 0xffff;
	debug("expanding boot note size to %u\n", new_size);
#ifdef HAVE_REALLOC
	bhdr = realloc(bhdr, new_size);
	bhdr->b_checksum = new_size;
#else
	printf("Boot notes too big\n");
	free(bhdr);
	return NULL;
#endif
    }

    addr = (char *) bhdr;
    addr += bhdr->b_size;
    memcpy(addr, &nhdr, sizeof(nhdr));
    addr += sizeof(nhdr);

    if (name && nhdr.n_namesz) {
        memcpy(addr, name, nhdr.n_namesz);
        addr += nhdr.n_namesz;
        pad = padded(nhdr.n_namesz) - nhdr.n_namesz;
        memset(addr, 0, pad);
        addr += pad;
    }

    memcpy(addr, desc, nhdr.n_descsz);
    addr += nhdr.n_descsz;
    pad = padded(nhdr.n_descsz) - nhdr.n_descsz;
    memset(addr, 0, pad);

    bhdr->b_size += ent_size;
    bhdr->b_records++;
    return bhdr;
}

static inline Elf_Bhdr *add_note_string(Elf_Bhdr *bhdr, const char *name,
	unsigned type, const char *desc)
{
    return add_boot_note(bhdr, name, type, desc, strlen(desc) + 1);
}

static Elf_Bhdr *build_boot_notes(struct sys_info *info, const char *cmdline)
{
    Elf_Bhdr *bhdr;

    bhdr = malloc(256);
    bhdr->b_signature = ELF_BHDR_MAGIC;
    bhdr->b_size = sizeof *bhdr;
    bhdr->b_checksum = 256; /* XXX cache the current buffer size here */
    bhdr->b_records = 0;

    if (info->firmware)
	bhdr = add_note_string(bhdr, NULL, EBN_FIRMWARE_TYPE, info->firmware);
    bhdr = add_note_string(bhdr, NULL, EBN_BOOTLOADER_NAME, program_name);
    bhdr = add_note_string(bhdr, NULL, EBN_BOOTLOADER_VERSION, program_version);
    if (cmdline)
	bhdr = add_note_string(bhdr, NULL, EBN_COMMAND_LINE, cmdline);
    if (!bhdr)
	return bhdr;
    bhdr->b_checksum = 0;
    bhdr->b_checksum = ipchksum(bhdr, bhdr->b_size);
    return bhdr;
}

int
is_elf(Elf_ehdr *ehdr)
{
    return (ehdr->e_ident[EI_MAG0] == ELFMAG0
        && ehdr->e_ident[EI_MAG1] == ELFMAG1
        && ehdr->e_ident[EI_MAG2] == ELFMAG2
        && ehdr->e_ident[EI_MAG3] == ELFMAG3
        && ehdr->e_ident[EI_CLASS] == ARCH_ELF_CLASS
        && ehdr->e_ident[EI_DATA] == ARCH_ELF_DATA
        && ehdr->e_ident[EI_VERSION] == EV_CURRENT
        && ehdr->e_type == ET_EXEC
        && ARCH_ELF_MACHINE_OK(ehdr->e_machine)
        && ehdr->e_version == EV_CURRENT
        && ehdr->e_phentsize == sizeof(Elf_phdr));
}

int
find_elf(Elf_ehdr *ehdr)
{
   int offset;

   for (offset = 0; offset < MAX_HEADERS * BS; offset += BS) {
        if ((size_t)read_io(fd, ehdr, sizeof ehdr) != sizeof ehdr) {
            debug("Can't read ELF header\n");
            return 0;
        }

        if (is_elf(ehdr)) {
            debug("Found ELF header at offset %d\n", offset);
	    return offset;
        }

        seek_io(fd, offset);
    }

    debug("Not a bootable ELF image\n");
    return 0;
}

Elf_phdr *
elf_readhdrs(int offset, Elf_ehdr *ehdr)
{
    unsigned long phdr_size;
    Elf_phdr *phdr;

    phdr_size = ehdr->e_phnum * sizeof(Elf_phdr);
    phdr = malloc(phdr_size);
    seek_io(fd, offset + ehdr->e_phoff);
    if ((size_t)read_io(fd, phdr, phdr_size) != phdr_size) {
	printf("Can't read program header\n");
	return NULL;
    }

    return phdr;
}

int 
elf_load(struct sys_info *info, ihandle_t dev, const char *cmdline, void **boot_notes)
{
    Elf_ehdr ehdr;
    Elf_phdr *phdr = NULL;
    unsigned long checksum_offset, file_size;
    unsigned short checksum = 0;
    int retval = -1;
    unsigned int offset;

    image_name = image_version = NULL;

    /* Mark the saved-program-state as invalid */
    feval("0 state-valid !");

    fd = open_ih(dev);
    if (fd == -1) {
	goto out;
    }

    offset = find_elf(&ehdr);
    if (!offset) {
	retval = LOADER_NOT_SUPPORT;
        goto out;
    }

#if DEBUG
	printk("ELF header:\n");
	printk(" ehdr.e_type    = %d\n", (int)ehdr.e_type);
	printk(" ehdr.e_machine = %d\n", (int)ehdr.e_machine);
	printk(" ehdr.e_version = %d\n", (int)ehdr.e_version);
	printk(" ehdr.e_entry   = 0x%08x\n", (int)ehdr.e_entry);
	printk(" ehdr.e_phoff   = 0x%08x\n", (int)ehdr.e_phoff);
	printk(" ehdr.e_shoff   = 0x%08x\n", (int)ehdr.e_shoff);
	printk(" ehdr.e_flags   = %d\n", (int)ehdr.e_flags);
	printk(" ehdr.e_ehsize  = 0x%08x\n", (int)ehdr.e_ehsize);
	printk(" ehdr.e_phentsize = 0x%08x\n", (int)ehdr.e_phentsize);
	printk(" ehdr.e_phnum   = %d\n", (int)ehdr.e_phnum);
#endif

    if (ehdr.e_phnum > MAX_HEADERS) {
        printk ("elfload: too many program headers (MAX_HEADERS)\n");
        retval = 0;
	goto out;
    }

    phdr = elf_readhdrs(offset, &ehdr);
    if (!phdr)
        goto out;

    if (!check_mem_ranges(info, phdr, ehdr.e_phnum))
	goto out;

    checksum_offset = process_image_notes(phdr, ehdr.e_phnum, &checksum, offset);

    printf("Loading %s", image_name ? image_name : "image");
    if (image_version)
	printf(" version %s", image_version);
    printf("...\n");

    if (!load_segments(phdr, ehdr.e_phnum, checksum_offset, offset, &file_size))
	goto out;

    if (checksum_offset) {
	if (!verify_image(&ehdr, phdr, ehdr.e_phnum, checksum))
	    goto out;
    }

    /* If we are attempting an ELF boot image, we pass a non-NULL pointer
       into boot_notes and mark the image as elf-boot rather than standard
       ELF */
    if (boot_notes) {
        *boot_notes = (void *)virt_to_phys(build_boot_notes(info, cmdline));
        feval("elf-boot saved-program-state >sps.file-type !");
    } else {
        feval("elf saved-program-state >sps.file-type !");
    }

    //debug("current time: %lu\n", currticks());

    debug("entry point is " FMT_elf "\n", addr_fixup(ehdr.e_entry));

    // Initialise saved-program-state
    PUSH(addr_fixup(ehdr.e_entry));
    feval("saved-program-state >sps.entry !");
    PUSH(file_size);
    feval("saved-program-state >sps.file-size !");

    feval("-1 state-valid !");

out:
    close_io(fd);
    if (phdr)
	free(phdr);
    if (image_name)
	free(image_name);
    if (image_version)
	free(image_version);
    return retval;
}

void 
elf_init_program(void)
{
	char *base;
	int i;
	Elf_ehdr *ehdr;
	Elf_phdr *phdr;
	size_t size, total_size = 0;
	char *addr;
	uintptr_t tmp;

	/* TODO: manage ELF notes section */
	feval("0 state-valid !");
	feval("load-base");
	base = (char*)cell2pointer(POP());

	ehdr = (Elf_ehdr *)base;

	if (!is_elf(ehdr)) {
		debug("Not a valid ELF memory image\n");
		return;
	}

	phdr = (Elf_phdr *)(base + ehdr->e_phoff);

	for (i = 0; i < ehdr->e_phnum; i++) {

#if DEBUG
		debug("filesz: %08lX memsz: %08lX p_offset: %08lX "
                        "p_vaddr %08lX\n",
			(unsigned long)phdr[i].p_filesz, (unsigned long)phdr[i].p_memsz,
			(unsigned long)phdr[i].p_offset, (unsigned long)phdr[i].p_vaddr );
#endif

		size = MIN(phdr[i].p_filesz, phdr[i].p_memsz);
		if (!size)
			continue;
#if !defined(CONFIG_SPARC32) && !defined(CONFIG_X86)
		if( ofmem_claim( phdr[i].p_vaddr, phdr[i].p_memsz, 0 ) == -1 ) {
                        printk("Ignoring failed claim for va %lx memsz %lx!\n",
                               (unsigned long)phdr[i].p_vaddr,
                               (unsigned long)phdr[i].p_memsz);
		}
#endif
		/* Workaround for archs where sizeof(int) != pointer size */
		tmp = phdr[i].p_vaddr;
		addr = (char *)tmp;

		memcpy(addr, base + phdr[i].p_offset, size);

		total_size += size;

#ifdef CONFIG_PPC
		flush_icache_range( addr, addr + size );
#endif
	}

	// Initialise saved-program-state
	PUSH(ehdr->e_entry);
	feval("saved-program-state >sps.entry !");
	PUSH(total_size);
	feval("saved-program-state >sps.file-size !");
	feval("elf saved-program-state >sps.file-type !");

	feval("-1 state-valid !");
}