#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>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Chih-Chung Chang <jochang@gmail.com>");
MODULE_PARM(kernel, "s");
MODULE_PARM(initrd, "s");
MODULE_PARM(cmdline, "s");
MODULE_PARM_DESC(kernel, "kernel file name");
MODULE_PARM_DESC(initrd, "initrd file name");
MODULE_PARM_DESC(cmdline, "kernel command line");

static char* kernel = "vmlinux";
static char* initrd = 0;
static char* cmdline = "root=/dev/hda1";

#define LOW_MEM 0x800000  /* no smaller than kernel + 1M (bss) + initrd */

void load_kernel(unsigned long pa_load_kernel,
                 unsigned long pa_kernel_buf,
                 unsigned long pa_initrd_buf,
                 unsigned long pa_cmdline_start,
                 unsigned long pa_cmdline_end);

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

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

/*
 *  Allocate a page with physical address >= LOWMEM
 */
static void **save;
static int saved_pages;
static void *alloc_high_page()
{
  void *ptr;
  if(!save)
  {
    save = vmalloc(((LOW_MEM+PAGE_SIZE-1)/PAGE_SIZE)*4);
    if(!save) return 0;
  }

  while(1)
  {
    ptr = kmalloc(PAGE_SIZE, GFP_KERNEL);
    if(!ptr) return 0;
    if(__pa(ptr) >= LOW_MEM) break;
    save[saved_pages++] = ptr;
  }
  return ptr;
}

static void free_saved_pages()
{
  if(save)
  {
    int i;
    for(i=0;i<saved_pages;i++)
      kfree(save[i]);
    vfree(save);
  }
}

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;
  size_t size, got, i;
  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;
  ibuf = (struct indirect_buffer*)alloc_high_page();
  if(!ibuf) goto out;
  memset(ibuf, 0, PAGE_SIZE);

  if(inode->i_size > MAX_INDIRECT_BUFFER_SIZE) goto out2;
  size = (size_t)inode->i_size;
  ibuf->size = size;

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

    err = -ENOMEM;
    buf = alloc_high_page();
    if(!buf) goto out2;
    ibuf->paddr[i/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 = ibuf;
  err = 0;

out:
  filp_close(file, NULL);
  return err;
out2:
  free_ibuffer(ibuf);
  goto out;
}

static void free_ibuffer(struct indirect_buffer *ibuf)
{
  int i;
  for(i=0;ibuf->paddr[i];i++)
    kfree((void *)__va(ibuf->paddr[i]));
  kfree(ibuf);
}

/* 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, *initrd_buf;
  int err;

  printk(KERN_INFO "loader module loaded\n");
  printk(KERN_INFO "kernel=%s\n", kernel);
  if(initrd) printk(KERN_INFO "initrd=%s\n", initrd);
  printk(KERN_INFO "cmdline=%s\n", cmdline);

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

  if(initrd)
  {
    if((err = read_file(initrd, &initrd_buf)))
      goto out2;
  }
  else
    initrd_buf = 0;

  load_kernel(va2pa(load_kernel),
              va2pa(kernel_buf),
              initrd_buf?va2pa(initrd_buf):0,
              va2pa(cmdline),
              va2pa(cmdline+strlen(cmdline)));

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

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