/* 
 * This file is subject to the terms and conditions of the GNU General Public
 * License.  See the file "COPYING" in the main directory of this archive
 * for more details.
 *
 * Copyright (C) Hewlett-Packard (Paul Bame) paul_bame@hp.com
 */
#include <stddef.h>
#include "bootloader.h"
#include <asm/pdc.h>
#include <asm/byteorder.h>
#include "load.h"

#undef PAGE0
#define PAGE0   ((struct zeropage *)0x00000000)

char commandline[CMDLINELEN];

int Debug = 0;
int interactive = 0;

void flush_data_cache(char *start, size_t length)
{
    char *end = start + length;

    do
    {
	asm volatile("fdc 0(%0)" : : "r" (start));
	asm volatile("fic 0(%%sr0,%0)" : : "r" (start));
	start += 16;
    } while (start < end);
    asm volatile("fdc 0(%0)" : : "r" (end));

    asm ("sync");
}

static int
parse_number(char *s, char **next)
{
    int n = 0;

    while (is_digit(*s))
    {
	n *= 10;
	n += *s++ - '0';
    }

    *next = s;

    return n;
}

static char *
parse_pfname(char *s, int *partition, char *name)
{
    char *p1, *p2 = NULL;

    if (s != NULL || *s != '\0')
    {
	/* parse the kernel partition number */
	*partition = parse_number(s, &p1);

	/* now the kernel name */
	p2 = strpbrk(p1, " \t");
	if (p2 != NULL)
	{
	    *p2 = '\0';
	    strcpy(name, p1);
	    *p2 = ' ';
	}
	else
	{
	    strcpy(name, p1);
	    p2 = p1 + strlen(p1);
	}
    }

    return p2;
}

/*
 * ext2_open_dirstrip()
 * Get filehandle for given filename.
 * If user gave 2/boot/vmlinux, don't fail and try if he maybe actually meant 2/vmlinux.
 */
static int
ext2_open_dirstrip(char *filename)
{
    while (filename && *filename) {
	char *last;
	int fh = ext2_open(filename);
	if (fh >= 0)
		return fh;

	last = filename;
	filename = strpbrk(&filename[1], "/");
	if (filename)
		printf("Warning: %s not found. Try %s instead...\n", last, filename);
    }

    return -1;
}

static int
chk_strcat(char *out, char *in, int len, int *ok)
{
    if (*ok)
    {
	int need = strlen(out) + strlen(in) + 1;
	if (need > len)
	{
	    printf("Adding '%s' exceeds length (%d)\n", in, len);
	    *ok = 0;
	}
	else
	{
	    strcat(out, in);
	}
    }
    return *ok;
}

/* return pointer to the commandline minus palo stuff */
static char *
parse(const char *cmdline, int *kpart, char *kname, int *rdpart, char *rdname)
{
    char buf[CMDLINELEN];
    static char lcmd[CMDLINELEN];
    char *suffix1, *suffix2;
    int ok = 1;

    /* need a copy to work on */
    strcpy(buf, cmdline);

    *kpart = -1;
    *rdpart = -1;

    suffix1 = parse_pfname(buf, kpart, kname);

    if (*suffix1 != '\0')
	suffix1++;
    strcpy(lcmd, suffix1);

    /* see if we have a ramdisk */
    suffix2 = suffix1;
    if ((strncmp(suffix1, "initrd=", 7) == 0) ||
             (suffix2 = strstr(suffix1, " initrd=")) != NULL)
    {
	char *suffix3;
	lcmd[suffix2 - suffix1] = '\0';
	if (*suffix2 == ' ')
	    suffix2++;
	suffix3 = parse_pfname(suffix2 + 7, rdpart, rdname);
	chk_strcat(lcmd, suffix3, sizeof lcmd, &ok);
    }

    return lcmd;
}

int
load_kernel(int fd, unsigned *entryp, int *wide)
{
    struct loadable loadable;
    int i;

    if (!prepare_loadable(fd, &loadable, wide))
    {
	printf("Couldn't grok your kernel executable format\n");
	return 0;
    }

    /* need to physicalize those huge addresses */
    loadable.entry = PHYS(loadable.entry);
    loadable.first = PHYS(loadable.first);

    printf("\nEntry %08x first %08x n %d\n",
		loadable.entry, loadable.first, loadable.n);

    for (i = 0; i < loadable.n; i++)
    {
	loadable.segment[i].mem =  PHYS(loadable.segment[i].mem);
	printf("Segment %d load %08x size %d mediaptr 0x%lx\n",
		i, loadable.segment[i].mem, loadable.segment[i].length,
		loadable.segment[i].offset);
    }

    if (!load_loadable((char *)loadable.first, fd, &loadable))
    {
	printf("Fatal error loading kernel executable\n");
	return 0;
    }

    flush_data_cache((char *)loadable.first, loadable.size);

    *entryp = loadable.entry;

    return 1;
}

static int
load_rd(int fd, int size)
{
    extern char *rd_start, *rd_end;
    char *rd;

    if (size <= 0)
	return 0;

    /* no idea if initrd space must be aligned, but once it was before... */
    rd = malloc_aligned(size, 4096);
    printf("Loading ramdisk %d bytes @ %p...", size, rd);
    if (seekread(fd, rd, size, 0) == size)
    {
	rd_start = rd;
	rd_end = rd + size;
    }
    flush_data_cache((char *)rd, size);
    puts("\n");

    return (rd_start != 0);
}

static void
join(char *out, int argc, char *argv[], int *ok)
{
    char tmpbuf[CMDLINELEN];
    int i;

    tmpbuf[0] = '\0';
    for (i = 0; i < argc; i++)
    {
	if (i > 0)
	    chk_strcat(tmpbuf, " ", sizeof tmpbuf, ok);
	chk_strcat(tmpbuf, argv[i], sizeof tmpbuf, ok);
    }
    strcpy(out, tmpbuf);
}

static struct diskpartition partition[MAXPARTS];
static int ext2 = 0;
static int bootdev;

static void
partition_transform(int *kern_part, int *rd_part)
{
    int i, palo_part = -1;

    /* if the F0 partition is the same as the requested kernel partition,
     * for now change it to zero to re-use the existing logic.  Should do
     * the reverse in the future probably.
     */
    for (i = 0; i < sizeof partition / sizeof partition[0]; i++)
    {
	if (partition[i].id == PALO_PARTITION)
	{
	    palo_part = i + 1;
	    if (*kern_part == palo_part)
		*kern_part = 0;

	    if (rd_part && *rd_part == palo_part)
		*rd_part = 0;

	    break;
	}
    }

    if (ext2 && *kern_part == 0) {
	*kern_part = palo_part;
	if(rd_part && *rd_part == 0)
	    *rd_part = palo_part;
    }

}

static void
ls(char *path)
{
    char  *p, kern_dir[256];
    const char *dir;
    int fd, part, part_fd;

    parse_pfname(path, &part, kern_dir);
    if ((p = strrchr(kern_dir, '/')) != NULL)
    {
	*++p = '.';
	*++p = '\0';
    }
    else
    {
	strcpy(kern_dir, "/.");
    }

    printf("Directory listing of %s\n\n", kern_dir);

    partition_transform(&part, NULL);

    /* partition table starts from zero */
    part_fd = offset_open(bootdev, 512 * partition[part - 1].start,
			      512 * partition[part - 1].length);
    if(ext2_mount(part_fd, 0, 0) == -1) {
	printf("Failed to mount partition %d\n", part);
	return;
    }

    if((fd = ext2_open(kern_dir)) == -1) {
	printf("Failed to open directory %s\n", kern_dir);
	return;
    }

    while((dir = ext2_readdir(fd, 0)) != NULL)
	if(dir[0] != '.') /* skip hidden files and . and .. */
	    printf("  %s\n", dir);
    
    printf("\n");
    ext2_close(fd);
    /* There's no umount ... since the next mount reuses
     * the current one's data structures ... */
}

static void
interact(int *ok)
{
    #define MAX_ARGV 40
    char *argv[MAX_ARGV], *p;
    char orig[CMDLINELEN];
    const char sep[] = " \t";
    char numbuf[4];
    char fieldbuf[200];
    int i, argc, editfield;

    strcpy(orig, commandline);

    while (1)
    {
	puts("Current command line:\n");
	puts(commandline);
	puts("\n");

	p = commandline;
	argc = 0;
	*ok = 1;
	do
	{
	    if (argc >= MAX_ARGV)
	    {
		argc = MAX_ARGV;
		break;
	    }
	    if ((argv[argc++] = strtok(p, sep)) == NULL)
	    {
		argc--;
		break;
	    }
	    p = NULL;
	} while (1);
	for (i = 0; i < argc; i++)
	{
	    printf("%2d: %s\n", i, argv[i]);
	}
	puts("\n"
	    "<#>    edit the numbered field\n"
	    "'b'    boot with this command line\n"
	    "'r'    restore command line\n"
	    "'l'    list dir\n"
	    "'x'    reset and reboot machine\n"
	    "? ");
	numbuf[0] = '0';
	numbuf[1] = '\0';
	enter_text(numbuf, sizeof numbuf - 1);
	puts("\n");

	if (numbuf[0] == 'b')
	{
	    join(commandline, argc, argv, ok);
	    break;
	}

	if (numbuf[0] == 'r')
	{
	    strcpy(commandline, orig);
	    continue;
	}

	if (numbuf[0] == 'l')
	{
	    join(commandline, argc, argv, ok);
	    ls(argv[0]);
	    continue;
	}

	if (numbuf[0] == 'x')
	    pdc_do_reset();

	editfield = parse_number(numbuf, &p);

	if (editfield >= MAX_ARGV)
	{
	    puts("Too many input fields.\n");
	}
	else if (editfield < argc)
	{
	    strncpy(fieldbuf, argv[editfield], sizeof(fieldbuf));
	    fieldbuf[sizeof(fieldbuf)-1] = '\0';
	    enter_text(fieldbuf, sizeof fieldbuf - 1);
	    puts("\n");
	    argv[editfield] = fieldbuf;
	}

	join(commandline, argc, argv, ok);
    }
}

unsigned
iplmain(int is_interactive, char *initialstackptr, int started_wide)
{
    extern char _end, _edata;
    int partitioned;
    unsigned entry;
    struct firstblock f;
    int blocked_bootdev;
    int wide;
    int kern_part, rd_part;
    char kern_name[128], rd_name[128];
    char kern_fullname[128];
    int ok = 1;

    /* BSS clear */
    bzero(&_edata, &_end - &_edata);

    /* heap grows down from initial stack pointer */
    malloc_init(initialstackptr);

    firmware_init(started_wide);
    putchar('p');	/* if you get this p and no more, string storage */
			/* in $GLOBAL$ is wrong or %dp is wrong */
    puts("alo ipl " PALOVERSION " ");
    puts(bld_info);
    puts("\n");
    interactive = is_interactive;
    if (Debug) printf("iplmain(%d, started %s)\n", interactive,
	started_wide ? "wide" : "narrow");
    if (Debug) printf("initial-sp %p\n", initialstackptr);

 restart:

    blocked_bootdev = pdc_bootdev_open();
    bootdev = byteio_open(blocked_bootdev);

    STRUCTREAD(bootdev, f, 0);
    if (strncmp(f.palomagic, PALOMAGIC, 4) != 0)
    {
	printf("ERROR: bad palo magic on boot device\n");
	while(1);
    }

    memset(&partition, 0, sizeof partition);
    partitioned = load_partitions(bootdev,
		partition, sizeof partition / sizeof partition[0]);

    if (partitioned)
    {
	printf("\n");
	print_ptab_pretty(partition, sizeof partition / sizeof partition[0]);
    }

    printf("\n%s contains:\n",
	partitioned ? "PALO(F0) partition" : "Boot image");

    if(partitioned && f.version >= 4 && (f.flags & PFLAG_EXT2)) {
	printf("PALO is formatted EXT2/3\n");
	ext2 = 1;
    }

    if (f.version < 3 && f.kern32_sz > 0)
    {
	printf("    0/vmlinux %d bytes @ 0x%x\n", f.kern32_sz, f.kern32_offset);
    }
    else
    {
        if (f.kern32_sz > 0)
	    printf("    0/vmlinux32 %d(%d) bytes @ 0x%x\n", f.kern32_sz, f.kern32_native_sz, f.kern32_offset);
        if (f.kern64_sz > 0)
	    printf("    0/vmlinux64 %d(%d) bytes @ 0x%x\n", f.kern64_sz, f.kern64_native_sz, f.kern64_offset);
    }

    if (f.rd_sz > 0)
	printf("    0/ramdisk %d bytes @ 0x%x\n", f.rd_sz, f.rd_offset);

    if (f.cmdline[0] == '\0' && f.cmdline_old[0]) /* old style command line ? */
    {
	strcpy(f.cmdline, f.cmdline_old);
	f.cmdline_old[0] = 0;
    }

    if (f.cmdline[0] == '\0')	/* no command line specified */
    {
	die("ERROR: No command line on boot media -- faking one\n");
	strcpy(f.cmdline, "0/vmlinux root=???");
	interactive = 1;
    }

    if (strlen(f.cmdline) >= sizeof f.cmdline)
	printf("WARNING: stored command line is longer than allowed\n");
    strcpy(commandline, f.cmdline);

    /* add the right console= if there isn't one yet */
    if (strstr(commandline, " console=") == 0)
    {
	printf("\nInformation: No console specified on kernel command line."
		" This is normal.\nPALO will choose the console currently"
		" used by firmware ");

        chk_strcat(commandline, " console=", CMDLINELEN, &ok);
	if (pdc_cons_duplex())
	{
            int is_mux;

	    printf("(serial).\n");

            if(pdc_cons_mux(&is_mux) != PDC_OK)
                printf("Information: The PDC calls to query the console device failed. Assuming console=ttyS0\n");

            if(is_mux)
	        chk_strcat(commandline, "ttyB0", CMDLINELEN, &ok);
            else
	        chk_strcat(commandline, "ttyS0", CMDLINELEN, &ok);

	    if (strstr(commandline, " TERM=") == 0)
	        chk_strcat(commandline, " TERM=vt102", CMDLINELEN, &ok);
	}
	else
	{
	    printf("(graphics).\n");
	    chk_strcat(commandline, "tty0", CMDLINELEN, &ok);
	    if (strstr(commandline, " sti=") == 0)
	    {
		struct {
		    unsigned char flags;
		    unsigned char bc[6];
		    unsigned char mod;
		} cons;
		int i;
		
		chk_strcat(commandline, " sti=", CMDLINELEN, &ok);
		if (pdc_read_conspath((unsigned char *)&cons) > 0)
		{
		    char pathcomp[4];
		    
		    for (i = 0; i < 6; i++)
		    {
			if (cons.bc[i] < 64)
			{
			    sprintf(pathcomp, "%d/", cons.bc[i]);
			    chk_strcat(commandline, pathcomp, CMDLINELEN, &ok);
			}
		    }
		    sprintf(pathcomp, "%d", cons.mod);
		    chk_strcat(commandline, pathcomp, CMDLINELEN, &ok);
		}
		else
		    chk_strcat (commandline, "0", CMDLINELEN, &ok);
	    }
	    if (strstr(commandline, " sti_font=") == 0)
	        chk_strcat(commandline, " sti_font=VGA8x16", CMDLINELEN, &ok);
	    if (strstr(commandline, " TERM=") == 0)
	        chk_strcat(commandline, " TERM=linux", CMDLINELEN, &ok);
	}
    }

    if (interactive)
	interact(&ok);

    /* If we have any failures after this, be sure we're interactive
     * for the re-start */
    interactive = 1;

    strcpy(commandline,
	    parse(commandline, &kern_part, kern_name, &rd_part, rd_name));

    sprintf(kern_fullname, "%d%s", kern_part, kern_name);
    chk_strcat(commandline, " palo_kernel=", CMDLINELEN, &ok);
    chk_strcat(commandline, kern_fullname, CMDLINELEN, &ok);

    printf("\nCommand line for kernel: '%s'\n", commandline);

    printf("Selected kernel: %s from partition %d\n", kern_name, kern_part);

    if (rd_part != -1)
	printf("Selected ramdisk: %s from partition %d\n", rd_name, rd_part);

    partition_transform(&kern_part, &rd_part);

    if (kern_part > 0 && !partitioned)
    {
	printf("ERROR: Requesting kernel from partition %d "
		    "on unpartitioned media!\n", kern_part);
	goto restart;
    }

    if (rd_part != -1 && rd_part != kern_part)
    {
	die("ERROR:: palo does not support ramdisk on different"
		" partition than kernel\n");
	goto restart;
    }

    if (kern_part == 0)
    {
	int kernfd;
	const char *wname;
	int rdfd;

	wname = kern_name + strlen(kern_name) - 2;
	if (wname >= kern_name && streq(wname, "32"))
	{
	    if (f.kern32_sz == 0)
	    {
	        die("Error: can't find a 32-bit kernel here");
		goto restart;
	    }
	    kernfd = offset_open(bootdev, f.kern32_offset, f.kern32_sz);
	}
	else if (wname >= kern_name && streq(wname, "64"))
	{
	    if (f.kern64_sz == 0)
	    {
	        die("Error: can't find a 64-bit kernel here");
		goto restart;
	    }
	    kernfd = offset_open(bootdev, f.kern64_offset, f.kern64_sz);
	}
	else
	{
	    if (f.version > 2)
		printf("Warning: kernel name doesn't end with 32 or 64 -- Guessing... ");

	    kernfd = -1;

	    if ((pdc_os_bits() & (OS_32|OS_64)) == (OS_32|OS_64))
	    {
		printf("\nThis box can boot either 32 or 64-bit kernels...\n");
		if (f.kern32_offset == 0 && f.kern64_offset != 0)
		{
		    printf("Only see a 64-bit kernel, using that\n");
		    kernfd = offset_open(bootdev, f.kern64_offset, f.kern64_sz);
		}
		else if (f.kern32_offset != 0 && f.kern64_offset == 0)
		{
		    printf("Only see a 32-bit kernel, using that\n");
		    kernfd = offset_open(bootdev, f.kern32_offset, f.kern32_sz);
		}
		else if (f.kern32_offset != 0 && f.kern64_offset != 0)
		{
		    printf("Both kernels available, choosing 64-bit kernel\n");
		    kernfd = offset_open(bootdev, f.kern64_offset, f.kern64_sz);
		}
		else
		{
		    die("No kernels found.");
		    goto restart;
		}
	    }

	    if (kernfd == -1 && (pdc_os_bits() & OS_64))
	    {
		printf("Choosing 64-bit kernel\n");
		kernfd = offset_open(bootdev, f.kern64_offset, f.kern64_sz);
	    }
	    else if (kernfd == -1 && (pdc_os_bits() & OS_32))
	    {
		printf("Choosing 32-bit kernel\n");
		kernfd = offset_open(bootdev, f.kern32_offset, f.kern32_sz);
	    }
	}

	/* FIXME!!! This *could* overwrite us -- probably should check */
	if (!load_kernel(kernfd, &entry, &wide))
	{
	    die("ERROR: failed to load kernel\n");
	    goto restart;
	}

	if (rd_part != -1)
	{
	    rdfd = offset_open(bootdev, f.rd_offset, f.rd_sz);
	    if (!load_rd(rdfd, f.rd_sz))
	    {
		printf("ERROR: failed to load ramdisk - proceeding anyway\n");
	    }
	}
    }
    else /* kern_part > 0 && we're partitioned */
    {
	int kern_fd;
	int bkern_fd;
	int rd_fd, brd_fd;
	int part_fd;
	int mount_fd;
	struct diskpartition *pp;

	if (kern_part >= MAXPARTS || 
	    (partition[kern_part - 1].id != LINUX_EXT2_PARTITION &&
	     partition[kern_part - 1].id != LINUX_RAID_PARTITION &&
	     partition[kern_part - 1].id != PALO_PARTITION) )
	{
	    printf("ERROR: Partition %d must be ext2\n", kern_part);
	    goto restart;
	}

	pp = &partition[kern_part - 1];
    
	part_fd = offset_open(bootdev, 512 * pp->start, 512 * pp->length);

	mount_fd = ext2_mount(part_fd, 0, 0);
	if (0) printf("ext2_mount(partition %d) returns %d\n",
	    kern_part, mount_fd);

	kern_fd = ext2_open_dirstrip(kern_name);
	if (0) printf("ext2_open_dirstrip(%s) = %d\n", kern_name, kern_fd);
	if (kern_fd < 0)
	{
	    printf("ERROR: open %s from partition %d failed\n",
		kern_name, kern_part);
	    goto restart;
	}

	bkern_fd = byteio_open(kern_fd);
	if (!load_kernel(bkern_fd, &entry, &wide))
	{
	    die("ERROR: failed to load kernel\n");
	    goto restart;
	}

	if (rd_part != -1)
	{
	    rd_fd = ext2_open_dirstrip(rd_name);
	    if(rd_fd >= 0) {
		brd_fd = byteio_open(rd_fd);

		if (!load_rd(brd_fd, ext2_filesize(rd_fd)))
		{
		    printf("ERROR: failed to load ramdisk - proceeding anyway\n");
		}
	    } else {
		printf("ERROR: failed to open ramdisk %s\n", rd_name);
	    }
	}
    }

    /* FIXME!!! need to pass command line to kernel */
    /* could theoretically use a function pointer, but they're ugly on PA */
    if(pdc_default_width(wide))
	    goto restart;
    printf("Branching to kernel entry point 0x%08x.  If this is the last\n"
	   "message you see, you may need to switch your console.  This is\n"
	   "a common symptom -- search the FAQ and mailing list at parisc-linux.org\n\n",
	   entry);
    return entry;
}
