/*
 *   pc partition support
 *
 *   Copyright (C) 2004 Stefan Reinauer
 *
 *   This code is based (and copied in many places) from
 *   mac partition support by Samuel Rydh (samuel@ibrium.se)
 *
 *   This program is free software; you can redistribute it and/or
 *   modify it under the terms of the GNU General Public License
 *   version 2
 *
 */

#include "config.h"
#include "libopenbios/bindings.h"
#include "libopenbios/load.h"
#include "libc/byteorder.h"
#include "libc/vsprintf.h"
#include "packages.h"

//#define DEBUG_PC_PARTS

#ifdef DEBUG_PC_PARTS
#define DPRINTF(fmt, args...)                   \
    do { printk(fmt , ##args); } while (0)
#else
#define DPRINTF(fmt, args...)
#endif

typedef struct {
	xt_t		seek_xt, read_xt;
	ucell	        offs_hi, offs_lo;
        ucell	        size_hi, size_lo;
	phandle_t	filesystem_ph;
} pcparts_info_t;

DECLARE_NODE( pcparts, INSTALL_OPEN, sizeof(pcparts_info_t), "+/packages/pc-parts" );

#define SEEK( pos )		({ DPUSH(pos); call_parent(di->seek_xt); POP(); })
#define READ( buf, size )	({ PUSH(pointer2cell(buf)); PUSH(size); call_parent(di->read_xt); POP(); })

/* three helper functions */

static inline int has_pc_valid_partition(unsigned char *sect)
{
	/* Make sure the partition table contains at least one valid entry */
	return (sect[0x1c2] != 0 || sect[0x1d2] != 0 || sect[0x1e2] != 0);
}

static inline int has_pc_part_magic(unsigned char *sect)
{
	return sect[0x1fe]==0x55 && sect[0x1ff]==0xAA;
}

static inline int is_pc_extended_part(unsigned char type)
{
	return type==5 || type==0xf || type==0x85;
}

/* ( open -- flag ) */
static void
pcparts_open( pcparts_info_t *di )
{
	char *str = my_args_copy();
	char *argstr = strdup("");
	char *parstr = strdup("");
	int bs, parnum=-1;
	int found = 0;
	phandle_t ph;
	ducell offs, size;

	/* Layout of PC partition table */
	struct pc_partition {
		unsigned char boot;
		unsigned char head;
		unsigned char sector;
		unsigned char cyl;
		unsigned char type;
		unsigned char e_head;
		unsigned char e_sector;
		unsigned char e_cyl;
		u32 start_sect; /* unaligned little endian */
		u32 nr_sects; /* ditto */
	} *p, *partition;

	unsigned char buf[512];

	DPRINTF("pcparts_open '%s'\n", str );

	/* 
		Arguments that we accept:
		id: [0-7]
		[(id)][,][filespec]
	*/

	if ( strlen(str) ) {
		/* Detect the arguments */
		if ((*str >= '0' && *str <= '7') || (*str == ',')) {
		    push_str(str);
		    PUSH(',');
		    fword("left-parse-string");
		    parstr = pop_fstr_copy();
		    argstr = pop_fstr_copy();
		} else {
		    argstr = str;
		}
			
		/* Convert the id to a partition number */
		if (parstr && strlen(parstr))
		    parnum = atol(parstr);
	}

	/* Make sure argstr is not null */
	if (argstr == NULL)
	    argstr = strdup("");
	
	DPRINTF("parstr: %s  argstr: %s  parnum: %d\n", parstr, argstr, parnum);
        free(parstr);

	if( parnum < 0 )
		parnum = 0;

	di->filesystem_ph = 0;
	di->read_xt = find_parent_method("read");
	di->seek_xt = find_parent_method("seek");

	SEEK( 0 );
	if( READ(buf, 512) != 512 )
		RET(0);

	/* Check Magic */
	if (!has_pc_part_magic(buf)) {
		DPRINTF("pc partition magic not found.\n");
		RET(0);
	}

	/* Actual partition data */
	partition = (struct pc_partition *) (buf + 0x1be);

	/* Make sure we use a copy accessible from an aligned pointer (some archs
	   e.g. SPARC will crash otherwise) */
	p = malloc(sizeof(struct pc_partition));

	bs = 512;

	if (parnum < 4) {
		/* primary partition */
		partition += parnum;
		memcpy(p, partition, sizeof(struct pc_partition));

		if (p->type == 0 || is_pc_extended_part(p->type)) {
			DPRINTF("partition %d does not exist\n", parnum+1 );
			RET( 0 );
		}

		offs = (long long)(__le32_to_cpu(p->start_sect)) * bs;
		di->offs_hi = offs >> BITS;
		di->offs_lo = offs & (ucell) -1;

		size = (long long)(__le32_to_cpu(p->nr_sects)) * bs;
        	di->size_hi = size >> BITS;
        	di->size_lo = size & (ucell) -1;

		DPRINTF("Primary partition at sector %x\n", __le32_to_cpu(p->start_sect));

		/* If PReP boot partition, exit immediately with no filesystem probe */
		if (p->type == 0x41) {
			RET(-1);
		}

		found = 1;
	} else {
		/* Extended partition */
		int i, cur_part;
		unsigned long ext_start, cur_table;

		/* Search for the extended partition
		 * which contains logical partitions */
		for (i = 0; i < 4; i++) {
			if (is_pc_extended_part(p[i].type))
				break;
		}

		if (i >= 4) {
			DPRINTF("Extended partition not found\n");
			RET( 0 );
		}

		DPRINTF("Extended partition at %d\n", i+1);

		/* Visit each logical partition labels */
		ext_start = __le32_to_cpu(p[i].start_sect);
		cur_table = ext_start;
		cur_part = 4;

		while (cur_part <= parnum) {
			DPRINTF("cur_part=%d at %lx\n", cur_part, cur_table);

			SEEK( cur_table * bs );
			if( READ(buf, sizeof(512)) != sizeof(512) )
				RET( 0 );

			if (!has_pc_part_magic(buf)) {
				DPRINTF("Extended partition has no magic\n");
				break;
			}

			/* Read the extended partition, making sure we are aligned again */
			partition = (struct pc_partition *) (buf + 0x1be);
			memcpy(p, partition, sizeof(struct pc_partition));

			/* First entry is the logical partition */
			if (cur_part == parnum) {
				if (p->type == 0) {
					DPRINTF("Partition %d is empty\n", parnum+1);
					RET( 0 );
				}

				offs = (long long)(cur_table+__le32_to_cpu(p->start_sect)) * bs;
				di->offs_hi = offs >> BITS;
				di->offs_lo = offs & (ucell) -1;

				size = (long long)__le32_to_cpu(p->nr_sects) * bs;
				di->size_hi = size >> BITS;
				di->size_lo = size & (ucell) -1;

				/* If PReP boot partition, exit immediately with no filesystem probe */
				if (p->type == 0x41) {
					RET(-1);
				}

				found = 1;
				break;
			}

			/* Second entry is link to next partition */
			partition = (struct pc_partition *) (buf + 0x1ce);
			memcpy(p, partition, sizeof(struct pc_partition));

			if (!is_pc_extended_part(p->type)) {
				DPRINTF("no link\n");
				break;
			}

			cur_table = ext_start + __le32_to_cpu(p->start_sect);
			cur_part++;
		}

		if (!found) {
			DPRINTF("Logical partition %d does not exist\n", parnum+1);
			RET( 0 );
		}
	}
	
	free(p);

	if (found) {
		/* We have a valid partition - so probe for a filesystem at the current offset */
		DPRINTF("pc-parts: about to probe for fs\n");
		DPUSH( offs );
		PUSH_ih( my_parent() );
		parword("find-filesystem");
		DPRINTF("pc-parts: done fs probe\n");
	
		ph = POP_ph();
		if( ph ) {
			DPRINTF("pc-parts: filesystem found with ph " FMT_ucellx " and args %s\n", ph, argstr);
			di->filesystem_ph = ph;

			/* If we have been asked to open a particular file, interpose the filesystem package with 
			the passed filename as an argument */
			if (strlen(argstr)) {
				push_str( argstr );
				PUSH_ph( ph );
				fword("interpose");
			}
		} else {
			DPRINTF("pc-parts: no filesystem found; bypassing misc-files interpose\n");
		}
	
		free( str );
		RET( -1 );
	} else {
		DPRINTF("pc-parts: unable to locate partition\n");

		free( str );
		RET( 0 );
	}
}

/* ( block0 -- flag? ) */
static void
pcparts_probe( pcparts_info_t *dummy )
{
	unsigned char *buf = (unsigned char *)cell2pointer(POP());

	DPRINTF("probing for PC partitions\n");

	/* We also check that at least one valid partition exists; this is because
	some CDs seem broken in that they have a partition table but it is empty
	e.g. MorphOS. */
	RET ( has_pc_part_magic(buf) && has_pc_valid_partition(buf) );
}

/* ( -- type offset.d size.d ) */
static void
pcparts_get_info( pcparts_info_t *di )
{
	DPRINTF("PC get_info\n");
	PUSH( -1 );		/* no type */
	PUSH( di->offs_lo );
	PUSH( di->offs_hi );
	PUSH( di->size_lo );
	PUSH( di->size_hi );
}

static void
pcparts_block_size( __attribute__((unused))pcparts_info_t *di )
{
	PUSH(512);
}

static void
pcparts_initialize( pcparts_info_t *di )
{
	fword("register-partition-package");
}

/* ( pos.d -- status ) */
static void
pcparts_seek(pcparts_info_t *di )
{
	long long pos = DPOP();
	long long offs, size;

	DPRINTF("pcparts_seek %llx:\n", pos);

	/* Seek is invalid if we reach the end of the device */
	size = ((ducell)di->size_hi << BITS) | di->size_lo;
	if (pos > size)
		RET( -1 );

	/* Calculate the seek offset for the parent */
	offs = ((ducell)di->offs_hi << BITS) | di->offs_lo;
	offs += pos;
	DPUSH(offs);

	DPRINTF("pcparts_seek parent offset %llx:\n", offs);

	call_package(di->seek_xt, my_parent());
}

/* ( buf len -- actlen ) */
static void
pcparts_read(pcparts_info_t *di )
{
	DPRINTF("pcparts_read\n");

	/* Pass the read back up to the parent */
	call_package(di->read_xt, my_parent());
}

/* ( addr -- size ) */
static void
pcparts_load( __attribute__((unused))pcparts_info_t *di )
{
	/* Invoke the loader */
	load(my_self());
}

/* ( pathstr len -- ) */
static void
pcparts_dir( pcparts_info_t *di )
{
	if ( di->filesystem_ph ) {
		PUSH( my_self() );
		push_str("dir");
		PUSH( di->filesystem_ph );
		fword("find-method");
		POP();
		fword("execute");
	} else {
		forth_printf("pc-parts: Unable to determine filesystem\n");
		POP();
		POP();
	}
}

NODE_METHODS( pcparts ) = {
	{ "probe",	pcparts_probe 		},
	{ "open",	pcparts_open 		},
	{ "seek",	pcparts_seek 		},
	{ "read",	pcparts_read 		},
	{ "load",	pcparts_load 		},
	{ "dir",	pcparts_dir 		},
	{ "get-info",	pcparts_get_info 	},
	{ "block-size",	pcparts_block_size 	},
	{ NULL,		pcparts_initialize	},
};

void
pcparts_init( void )
{
	REGISTER_NODE( pcparts );
}
