#include <linux/module.h>
#include <linux/kernel.h>
#include <asm/uaccess.h>
#include <linux/file.h>
#include <linux/mm.h>
#include <linux/vmalloc.h>
#include <linux/slab.h>
#ifdef REBOOT_HOOK
#include <linux/reboot.h>
#endif

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Chih-Chung Chang <jochang@gmail.com>, Patched by Eiji Kawauchi <E.Kawauchi@gmail.com>");
MODULE_PARM(kernel, "s");
MODULE_PARM(cmdline, "s");
MODULE_PARM(startaddr, "i");
MODULE_PARM(bootdev, "i");
MODULE_PARM(boothowto, "i");
MODULE_PARM_DESC(kernel, "NetBSD kernel file name");
MODULE_PARM_DESC(cmdline, "NetBSD kernel command line");
MODULE_PARM_DESC(startaddr, "NetBSD kernel load-and-go address");
MODULE_PARM_DESC(bootdev, "NetBSD boot device");
MODULE_PARM_DESC(boothowto, "NetBSD boot howto flags");

static char* kernel = "netbsd.bin";
static char* cmdline = "";
static unsigned long startaddr = 0x90000;
static unsigned long bootdev = 0;
static unsigned long boothowto = 0;

extern char bootstr[];

#define LOW_MEM (30*1024*1024)	/* 30MB */

void load_kernel(unsigned long pa_load_kernel,
                 unsigned long pa_kernel_buf,
                 unsigned long pa_start,
                 unsigned long boot_device,
                 unsigned long boot_flags);

struct indirect_buffer {
	struct indirect_buffer *next;
	unsigned long paddr[1023];	/* physical address of each 4K page */
								/* terminate with zero */
};

#define MAX_INDIRECT_BUFFER_SIZE ((PAGE_SIZE/4-1)*PAGE_SIZE)

#ifdef REBOOT_HOOK
static int
loader_hook(struct notifier_block *self, unsigned long event, void *buf)
{
	return 0;	/* DUMMY */
}
struct notifier_block loader_reboot_nb = {loader_hook, NULL, INT_MAX};
#endif

/*
 *  Allocate a page with physical address >= LOWMEM
 */
static void **save;
static int saved_pages;

static void *
alloc_high_page(void)
{
	void *ptr;

	if (!save) {
		save = vmalloc(((LOW_MEM+PAGE_SIZE-1)/PAGE_SIZE)*4);
		if (!save)
			return NULL;
	}

	while(1) {
		ptr = kmalloc(PAGE_SIZE, GFP_KERNEL);
		if (!ptr) {
			printk(KERN_INFO "kmalloc failed!\n");
			return NULL;
		}
		if (__pa(ptr) >= LOW_MEM)
			break;
		save[saved_pages++] = ptr;
	}
	return ptr;
}

static void
free_saved_pages(void)
{
	if (save) {
		int i;

		for (i = 0; i < saved_pages; i++)
			kfree(save[i]);
		vfree(save);
	}
	return;
}

static void free_ibuffer(struct indirect_buffer *ibuf);

/*
 *  Read input file into an indirect buffer
 */
static int
read_file(char *filename, struct indirect_buffer **indirect_buf)
{
	struct file* file;
	struct inode* inode;
	struct indirect_buffer *ibuf;
	struct indirect_buffer *top;
	size_t size, got, i, j;
	mm_segment_t fs;
	int err;

	file = filp_open(filename, O_RDONLY, 0);
	if (IS_ERR(file))
		return PTR_ERR(file);

	err = -EIO;
	if (!file->f_op || !file->f_op->read)
		goto out;

	err = -EACCES;
	inode = file->f_dentry->d_inode;
	if (!S_ISREG(inode->i_mode))
		goto out;

	err = -ENOMEM;
	top = ibuf = (struct indirect_buffer*)alloc_high_page();
	if (!ibuf)
		goto out;
	memset(ibuf, 0, PAGE_SIZE);

#if 0
	if (inode->i_size > MAX_INDIRECT_BUFFER_SIZE) goto out2;
#endif

	size = (size_t)inode->i_size;

	for (i = 0, j = 0; i < size; i += PAGE_SIZE, j += PAGE_SIZE) {
		size_t todo = min(size-i, (size_t)PAGE_SIZE);
		void *buf;

		if (j >= PAGE_SIZE * 1023) {
			void *p;

			p = alloc_high_page();
			if (!p)
				goto out2;
			memset(p, 0, PAGE_SIZE);
			ibuf->next = (struct indirect_buffer *)__pa(p);
			ibuf = p;
			j = 0;
		}

		err = -ENOMEM;
		buf = alloc_high_page();
		if (!buf)
			goto out2;
		ibuf->paddr[j/PAGE_SIZE] = __pa(buf);

		err = -EIO;
		file->f_pos = i;
		fs = get_fs();
		set_fs(KERNEL_DS);
		got = file->f_op->read(file, buf, todo, &file->f_pos);
		set_fs(fs);
		if (got != todo)
			goto out2;
	}

	*indirect_buf = top;
	err = 0;
out:
	filp_close(file, NULL);
	return err;
out2:
	free_ibuffer(top);
	goto out;
}

static void
free_ibuffer(struct indirect_buffer *ibuf)
{
	int i;
	struct indirect_buffer *next;

	for (;;) {
		next = ibuf->next;

		for (i = 0; i < 1023; i++) {
			if (ibuf->paddr[i] == 0)
				break;
			kfree((void *)__va(ibuf->paddr[i]));
		}

		kfree(ibuf);

		if (next == NULL)
			break;

		ibuf = (struct indirect_buffer *)__va(next);
	}

	return;
}

/* convert vmalloc'ed memory to physical address */
static unsigned long
va2pa(void *p)
{
	return iopa((unsigned long)p);
}

int
init_module(void)
{
	struct indirect_buffer *kernel_buf;
	int err;

	printk(KERN_INFO "NetBSD loader module loaded\n");
	printk(KERN_INFO "kernel=%s\n", kernel);
	printk(KERN_INFO "load address=0x%08lx\n", startaddr);
	printk(KERN_INFO "boot device=0x%08lx\n", bootdev);
	printk(KERN_INFO "boot flags=0x%08lx\n", boothowto);
	if (cmdline[0]) {
		printk(KERN_INFO "cmdline=%s\n", cmdline);
		strncpy(bootstr, cmdline, 255);
	}
#if 0
	{
		unsigned long xaddr;
		xaddr = va2pa(load_kernel);
		printk(KERN_INFO "load_kernel_addr=0x%08lx\n", xaddr);
	}
#endif

	if ((err = read_file(kernel,&kernel_buf)))
		goto out;

#ifdef REBOOT_HOOK
	fsync_dev(0);
	register_reboot_notifier(&loader_reboot_nb);
	notifier_call_chain(&(loader_reboot_nb.next), SYS_RESTART, NULL);
#endif

 	load_kernel(va2pa(load_kernel), va2pa(kernel_buf), startaddr, bootdev,
			boothowto);

#ifdef REBOOT_HOOK
	unregister_reboot_notifier(&loader_reboot_nb);
#endif

	free_ibuffer(kernel_buf);
out:
	free_saved_pages();
	return err;
}

void
cleanup_module(void)
{
	printk(KERN_INFO "NetBSD loader module unloaded\n");
}
