/*

	DF.C - A utility to format a disk.

	This routine was developed for use on a Palomax hard drive system,
	which did not support bad sector re-mapping. It creates a file 
	called 'bad.blocks' which contains a valid file header with links
	to all of the bad blocks. It will work under both filing systems,
	and for hard and floppy drives. If nothing else, it has a better
	syntax than AmigaDos's, and hopefully, provides some examples of
	working with the filing system.

	Note: This utility will work with the Maximum drive size 
	currently supported by Palomax software, (2048 tracks and
	16 heads) and then some.

	Note also: Some of the error messages are meant mainly for hard
	drives, but should still be informative.
*/


#include	<exec/io.h>
#include	<exec/devices.h>
#include	<exec/memory.h>
#include	<devices/trackdisk.h>
#include	<devices/bootblock.h>
#include	<libraries/dosextens.h>
#include	<libraries/filehandler.h>
#include	"df.h"



char	Usage[] =
"<device name (df0: etc)> <options> [<volume name>]\n\
Where <options> is one or more of the following:\n\
	QUICK FORMAT\t\t -q\n\
	NO VERIFY\t\t -nv\n\
	PRINT BAD BLOCK LIST\t -p\n\
	UPDATE BAD BLOCK FILE\t -u\n\
	USE FAST FILING SYSTEM\t -ffs\n\
	ENTER BAD TRACK LIST\t -t\n";


char	Greeting[] = 
"	Amiga disk foramatter\n\
 	Copyright 1989 SLADE software\n\n";


char			*my_name;	/* Invocation name 		*/
struct 	MsgPort		*diskport;	/* Reply port for I/O requests	*/
struct	IOStdReq	*diskreq;	/* Request struct for I/O 	*/
char			*dsk_buf;	/* buffer to hold info from dsk */
long			doe;		/* Device open error flag 	*/
int			dev_open;	/* Device open flag 		*/
int			drive_inhibited;/* Flag for inhibit operation	*/
int			bad_tracks;	/* Number of bad tracks 	*/

int			dformat;	/* Flag to indicate format 	*/
int			update;		/* Flag to indicate just update */
int			quick_format;	/* Quick format flag...		*/
int			no_verify;	/* Format w/no verify flag..	*/
int			report;		/* Report on bad blocks only 	*/
int			enter_tracks;	/* Enter own bad tracks flag	*/

struct	RootBlock	rb;		/* Root block structure */
struct	disk_parm	dp;		/* disk_parm structure	*/

extern	void		update_bad_blocks();
extern	void		create_bad_block_file();
extern	void		print_bad_blocks();
extern	int		block_already_bad();

struct	bad_block	*bad_block_list;  /* Used block list pointer	*/
int	bad_blocks;			  /* Number of used/bad blocks	*/
void	add_bad_block();		  /* add bad block routine	*/

long	bad_track_list[BADTRKSIZE];	  /* Bad track list		*/
char	*device;			  /* Device (df0: etc) to use	*/
char	volname[40];			  /* Supplied volume name	*/

extern	struct	DosLibrary	*DOSBase;
int	def_PreAlloc;		


/*-----------------------------------------------------------------------------
------------------------------------------------------------------------------*/
BYTE	write_blocks( block_start, count )
ULONG	block_start;
int	count;
	{
	/*
	  Write count blocks to the disk. block_start contains the
	  the actual block number to write. We do not check the
	  range here. The write will be immediate with no buffering
	  because we do a CMD_UPDATE after the write...
	*/

	diskreq->io_Length 	= dp.block_size*count;
	diskreq->io_Data   	= (APTR) dsk_buf;
	diskreq->io_Command	= CMD_WRITE;
	diskreq->io_Offset	= block_start*dp.block_size;
	DoIO( diskreq );
	if ( diskreq->io_Error )
  		return( (diskreq->io_Error == 0) );

	diskreq->io_Command	= CMD_UPDATE;
	DoIO( diskreq );
	return( (diskreq->io_Error == 0) );
	}


/*-----------------------------------------------------------------------------
------------------------------------------------------------------------------*/
BYTE	read_blocks( block_start, count )
ULONG	block_start;
int	count;
	{
	/*
	  Read count blocks from the file. block_start contains the
	  the actual starting offset of the block to read. We do not 
	  check the range here!
	*/

	diskreq->io_Length 	= dp.block_size*count;
	diskreq->io_Data   	= (APTR) dsk_buf;
	diskreq->io_Command	= CMD_READ;
	diskreq->io_Offset	= block_start;
	DoIO( diskreq );
	return( (diskreq->io_Error == 0) );
	}


/*-----------------------------------------------------------------------------
------------------------------------------------------------------------------*/
void	motor_off()
	{
	/*
	  Turn the drive motor off. (only valid for 3.5's)...
	*/
	diskreq->io_Length 	= 0;
	diskreq->io_Command	= TD_MOTOR;
	DoIO( diskreq );
	}



/*-----------------------------------------------------------------------------
------------------------------------------------------------------------------*/
void	inhibit_drive( yorn )
int	yorn;
	{
	/*
	  Depending on the value of yorn, inhibit or enable the drive.
	*/

	void		df_exit();
	struct	MsgPort	*dev_proc, *DeviceProc();

	/*
	  Find the device handling this drive, and complain if it
	  does not exist.
	*/
	if ( !(dev_proc = DeviceProc( device ) ) )
		{
		df_exit( EBADDEV );
		}

	/*
	   Tell DOS to inhibit the drive.
	*/
	if ( !dos_packet( dev_proc, ACTION_INHIBIT, (long) yorn,
			  0L, 0L, 0L, 0L, 0L, 0L, 0L ) ) 
		{
		df_exit( ENOINH );	
		}

	drive_inhibited	= yorn;
	}


/*-----------------------------------------------------------------------------
------------------------------------------------------------------------------*/
BYTE	verify_track( track )
ULONG	track;
	{
	/*
	  Do a read-verify of the track we just formatted. If no_verify
	  is true, just return true...
	*/
	long	track_start;

	if ( no_verify )
		return( 1 );

	track_start = track * dp.track_size;

	read_blocks( track_start, dp.blocks_track );
	return( (diskreq->io_Error == 0) );
	}



/*-----------------------------------------------------------------------------
------------------------------------------------------------------------------*/
BYTE 	format_track( track )
ULONG	track;
	{
	/* 
	  Tell the device driver to format the supplied track. A track
	  is defined by disk_parm.track_size.
	*/
	
	diskreq->io_Length 	= dp.track_size;
	diskreq->io_Data   	= (APTR) dsk_buf;
	diskreq->io_Command	= TD_FORMAT;
	diskreq->io_Offset	= track * dp.track_size;
	DoIO( diskreq );
		
	return( (diskreq->io_Error == 0) );
	}
	


/*-----------------------------------------------------------------------------
------------------------------------------------------------------------------*/
void	format_disk()
	{
	/* 
	  Format the disk a track at a time from low_cyl to high_cyl on 
	  dp.surfaces heads. If we come to a track that is already in the
	  bad_track_list, skip the format. If we find a new bad track, 
	  add it to the list, and make sure the list is not full.
	*/
	void	df_exit();
	void	initialize_disk();
	ULONG	i, j, cur_track;

  
	inhibit_drive( 1 );

	/*
	  We have several options here: Normal format and verify,
	  format without verify, and quick format (write root and 
	  boot blocks only). Test for quick first...
	*/
	if ( quick_format )
		{
		initialize_disk();
		return;
		}

	for( i = dp.low_cyl; i <= dp.high_cyl; i++ )
		for ( j = 0; j < dp.surfaces; j++ )
		{
		cur_track = (i * dp.surfaces) + j;

		if ( track_already_bad( cur_track ) )
			{
			printf( "Head %ld, Track %ld already marked as bad - skipping\n", 
				 j, i );
			continue;
			}

		if ( !format_track( cur_track ) || !verify_track( cur_track ) )
			{
			bad_track_list[bad_tracks++] = cur_track;
			if ( bad_tracks > BADTRKSIZE )
				{
				df_exit( EBTOV );
				}

			printf( "Unable to format head: %ld, track: %ld ", j, i );
			printf( "Adding to bad track list\n" );
			}

		}

	/*
	  Initialize the disk...
	*/
	initialize_disk();
	}


/*-----------------------------------------------------------------------------
------------------------------------------------------------------------------*/
void	create_bad_block_list()
	{
	/* 
	  Take the bad track table and convert the tracks to blocks.
	  The first block on a track will be (blocks_track * track),
	  then we just count up to blocks_track to get the rest.
	*/
	int			i, j;
	ULONG			current_block;

	for( i = 0, bad_blocks = 0; i < bad_tracks; i++ )
		{
		current_block = bad_track_list[i] * dp.blocks_track;

		for( j = 0; j < dp.blocks_track; j++ )
			{
			add_bad_block( current_block++ );
			bad_blocks++;
			}
		}
	}


/*-----------------------------------------------------------------------------
------------------------------------------------------------------------------*/
void	write_boot_block()
	{
	/*
	  Create the boot block and write it to the disk. We will first
	  check to see if it was found to be bad during the format. If
	  so, we will inform the user and quit. Also, if we get a write
	  error (in case we only did a read verify), we will quit.
	*/
	void	df_exit();
	int	i;
	
	if ( block_already_bad( dp.boot_key ) )
		{
		df_exit( EBADBB );
		}

	switch( dp.disk_type )
		{
		case	ROMFS : strcpy( dsk_buf, "DOS" );
				break;

		case	FFS   : strcpy( dsk_buf, "DOS\001" );
		}

	if ( !write_blocks( dp.boot_key, 1 ) )
		{
		df_exit( EBADBB );
		}
	}



/*-----------------------------------------------------------------------------
------------------------------------------------------------------------------*/
void	initialize_root_block()
	{
	/*
	  Initialize the known fields in the root block. 
	*/

	rb.Type 		= 2L;
	rb.SecondaryType 	= 1L;
	rb.HTSize		= 72L;

	/*
	   Set the BimapFlag to 0, so the validator will come and 
	   create the bitmap for us.....
	*/
	rb.BitmapFlag		= 0L;

	/*
	  Copy the volume name to the root block.
	*/
	rb.Name[0] = strlen( volname );
	strcpy( &rb.Name[1], volname );
	
	/*
	  Create and initialize all the pertinent date fields: DiskMade,
	  DirAltered, and DiskAltered. I set these to the same value due
	  to ignorance. If they should be different, feel free to change
	  this.
	*/
	DateStamp( &rb.DiskMade[0] );
	movmem( (char *) &rb.DiskMade, (char *) &rb.DirAltered, 12);
	movmem( (char *) &rb.DiskMade, (char *) &rb.DiskAltered, 12);
	}


/*-----------------------------------------------------------------------------
------------------------------------------------------------------------------*/
void	write_root_block()
	{
	/*
	  Calculate the checksum for the root block and attempt to write
	  it to the disk.
	*/
	int	i;
	long	temp, *p;

	/*
	   Calculate the checksum for this block.
	*/
	for( i = 0, temp = 0, p = (long *) &rb; i < (sizeof( rb )/4); i++ )
		{
		temp += *p++;
		}
	rb.Checksum = -temp;


	/*
	  Copy it to the disk buffer and write it.
	*/
	movmem( (char *) &rb, dsk_buf, sizeof( rb ) );
	if ( !write_blocks( dp.root_key, 1 ) )
		{
		df_exit( EBADRB );
		}
	}



/*-----------------------------------------------------------------------------
------------------------------------------------------------------------------*/
void	initialize_disk()
	{
	/*
	  Attempt to write the boot block, root block and bad block file.
	  each routine will handle its own errors. We do not mess with the
	  bitmap for the disk at all. We set the 'not validated' field of
	  the root block, and when we un-inibit() the drive, AmigaDOS will
	  go out and create it for us. Not the most efficient thing to do,
	  but it beats writing the code to do it by hand...
	*/
	create_bad_block_list();	/* translate tracks to blocks 	*/
	write_boot_block();		/* write the boot block		*/
	initialize_root_block();	/* set some fields in root block*/

	/* 
	  Create and write the bad block file if we have any bad blocks.
	*/
	if ( bad_block_list )
		create_bad_block_file();

	write_root_block();		/* update and write the root blk*/
	}



/*-----------------------------------------------------------------------------
------------------------------------------------------------------------------*/
get_drive_type()
	{
	/*
	  If we are going to format the drive, its type will be ROMFS by
	  default, unless the user specified -ffs. If we are just going
	  to do an update, we need to read the boot block to get the type.
	*/
	void	df_exit();
	
	if ( update )
		{
		if ( !read_blocks( dp.boot_key, 1 ) )
			{
			df_exit( EBADBB );
			}

		dp.disk_type = (int) dsk_buf[3];
		}

	/*
	  Get the pre-allocated block count.
	*/
	dp.pre_alloc = (dp.disk_type == ROMFS) ? 0 : def_PreAlloc;

	/*
	  dp.lower_key is the lowest block on the drive. It is usually 2, 
	  because AmigaDOS reserves the lowest two for the boot blocks - 
	  also because of the way the bitmap is structured, we cannot use 
	  block 0 - a value of 0 in the bitmap indicates a used block.
	*/
	dp.lower_key	= dp.low_cyl * dp.blocks_track * dp.surfaces + 
			  dp.reserved;

	dp.upper_key	= ((dp.high_cyl + 1) * dp.surfaces * dp.blocks_track) -
			  ( dp.pre_alloc + 1 ); 

	}



/*-----------------------------------------------------------------------------
------------------------------------------------------------------------------*/
get_drive_stats( drive )
char	*drive; 
	{
	/*
	  Get the needed drive data from the environment vector. This is also
	  the DevInfo block masquerading under a different name. It is not too
	  well documented, and I have TransAmi to thank for this little tidbit.
	*/

	struct RootNode			*rn;
	struct DosInfo			*dsi;
	struct DeviceNode		*di;
	struct FileSysStartupMsg	*fsm;
	struct DosEnvironment		*de;
	char				dev[32], *p, *q;
	
	for( p = drive, q = dev; (*p != ':') && *p; )
		*q++ = toupper( *p++ );

	*q = '\0';

	if ( !p )
		{
		return( 0 );
		}

	Forbid();
	rn = (struct RootNode *) DOSBase->dl_Root;
	dsi= (struct DosInfo *) BADDR( rn->rn_Info );
	di = (struct DeviceNode *) BADDR( dsi->di_DevInfo );

	/*
	  Attempt to find the device in the list.
	*/
	for( ; di->dn_Next; di = (struct DeviceNode *) BADDR( di->dn_Next ) )
		{
		if ( !strcmp( BADDR( di->dn_Name )+1, dev ) )
			break;
		}

	if ( !di )		/* device not found */
		{
		Permit();
		return( 0 );
		}


	fsm = (struct FileSysStartupMsg *) BADDR( di->dn_Startup );
	de =  (struct DosEnvironment *) BADDR( fsm->fssm_Environ );

	/*
	  Copy the pertinent fields.
	*/
	dp.unit		= fsm->fssm_Unit;		/* Unit number 	    */
	dp.block_size	= de->de_SizeBlock * 4;		/* Block size  	    */
	dp.surfaces	= de->de_Surfaces;		/* Sides	    */
	dp.sec_block	= de->de_SecPerBlock;		/* Sectors/block    */
	dp.blocks_track = de->de_BlocksPerTrack;	/* Blocks/track	    */
	dp.reserved	= de->de_Reserved;		/* Reserved blocks  */
	dp.low_cyl	= de->de_LowCyl;		/* Low cylinder	    */
	dp.high_cyl	= de->de_HighCyl;		/* High cylinder    */

	/*
	  Copy the name of the driver to dp.driver.
	*/
	p 		= (char *) BADDR( fsm->fssm_Device );
	strncpy( dp.driver, p + 1, (int) *p );

	dp.track_size	= dp.block_size * dp.blocks_track;

	dp.root_key	= ((dp.high_cyl + 1) * dp.blocks_track * dp.surfaces)/2;
	dp.boot_key	= dp.low_cyl * dp.surfaces * dp.blocks_track;

	def_PreAlloc 	= de->de_PreAlloc;
	Permit();
	return( 1 );
	}




/*-----------------------------------------------------------------------------
------------------------------------------------------------------------------*/
void	init_bad_track_list()
	{
	/* 
	  Initialize the bad track list to -1's.
	*/
	int 	i;

	for( i = 0; i < BADTRKSIZE; i++ )
		bad_track_list[i] = -1L;
	}


/*-----------------------------------------------------------------------------
------------------------------------------------------------------------------*/
void	get_bad_tracks()
	{
	/*
	  Prompt the user for a list of bad tracks.
	*/
	char	tmp[10];
	ULONG	cur_track, head, track;

	printf( "Enter bad tracks - enter q to quit\n" );

	for ( ; ; )
		{
		printf( "Head " );
		gets( tmp );
		if ( toupper( *tmp ) == 'Q' )
			break;
		
		head = atol( tmp );
		printf( "Track " );
		gets( tmp );
		track = atol( tmp ); 

		cur_track = (track * dp.surfaces) + head;	


		bad_track_list[bad_tracks++] = cur_track;
		if ( bad_tracks > BADTRKSIZE )
			{
			df_exit( EBTOV );
			}
		}

	}



/*-----------------------------------------------------------------------------
------------------------------------------------------------------------------*/
int	track_already_bad( track )
ULONG	track;
	{
	/*
	   Cruise through the bad_track_list and see if the given track
	   is in it. return the result of the search.
	*/
	int	i;

	for( i = 0; i < BADTRKSIZE; i++ )
		{
		if ( bad_track_list[i] == track ) 
			return( 1 );
		}
	return( 0 );
	}



/*-----------------------------------------------------------------------------
------------------------------------------------------------------------------*/
void	df_exit( rc, err )
int	rc;
char	*err;
	{
	/*
	  This is the function that everyone exits by. We will
	  close or return allocated resources and quit gracefully
	  on error.
	*/

	if ( (dev_open) && (dp.disk_type == ROMFS) ) 
		motor_off();

	if ( dev_open )
		CloseDevice( diskreq );

	if ( diskport )
		DeletePort( diskport );

	if ( diskreq )
		DeleteStdIO( diskreq );

	if ( dsk_buf )
		FreeMem( dsk_buf, (long) dp.track_size );

	if ( drive_inhibited )
		inhibit_drive( 0 );

	if ( !rc )
	    exit( rc );


	if ( rc == ENOARGS )
	    	{
		printf( "Usage: %s %s\n", my_name, Usage );
		exit( 200 );
		}

	/* Tell user where the error came from */
	printf( "%s:", my_name );

	switch ( rc )
		{
		case	ENOPORT	  : printf( "Cannot open reply port\n" );
				    break;

		case	ENOIOBLK  : printf( "Cannot create I/O block\n" );
			 	    break;

		case 	ENOOPEN	  : printf( "Cannot open device - error: %x\n", doe );
		    		    break;

		case	EBADDEV	  : printf( "Illegal device name\n" );
		    		    break;

		case	EBTOV	  : printf( "Bad track table overflow\n" );
				    break;

		case	ENOMEM	  : printf( "Unable to allocate memory\n" );
		    		    break;

		case	EBADBB    : printf( "The boot block is not writeable. " );
			    	    printf( "If this is a Mounted disk, you\n");
			    	    printf( "must increase the LowCyl value " );
			    	    printf( "in the Mountlist to format this ");
				    printf( "disk.\n" );
				    break;

		case	EBADRB    : printf( "The root block is not writeable. " );
				    printf( "You must change either the low_cyl\n" );
				    printf( "or high_cyl value in the Mountlist " );
				    printf( "to format this disk\n" );
				    break;

		case   ENOBLKS	  : printf( "No more room to create bad " );
				    printf( "block list\n" );
				    break;

		case   	EBMAPWR	  : printf( "Another bad block found while " );
				    printf( "writing the bitmap - please " );
				    printf( "re-run\n" );
				    break;

		case	EBADOPT	  : printf( "Bad option: %s Halting\n", err );
				    break;

		case	ENOINH	  : printf( "Unable to inhibit drive\n" );
				    break;

		case	ENHDR	  : printf( "Unable to write file header block " );
				    printf( "to the disk - format failed\n" );
				    break;

		case	ENEXTB	  : printf( "Unable to write file extension " );
				    printf( "blocks to the disk - format failed\n");
				    break;

		case	EBADBLK	  : printf( "Block number out of range\n" );
				    break;
		}


	exit( 200 );
	}



/*-----------------------------------------------------------------------------
------------------------------------------------------------------------------*/
void	parse_args( arg_list )
char	**arg_list;
	{
	/* 
	  See if we got any flags, and assign some global variables. Assume
	  a normal format (dformat = True) if no args are supplied.
	*/
	dformat		= 1;
	report		=
	no_verify	=
	update		=
	enter_tracks	=
	quick_format	= 0;
	dp.disk_type	= ROMFS;		/* default */

	/*
	  The device name MUST be next...
	*/
	device	= *arg_list;

	/*
	  Now all we have left is flags and possibly a volume name. 
	*/

	while( *++arg_list )
		{
		if (!strcmp( *arg_list, "-u" ) ) 
			{
			update = 1;
			continue;
			}

		if (!strcmp( *arg_list, "-p" ) ) 
			{
			report = 1;
			continue;
			}

		if (!strcmp( *arg_list, "-ffs" ) )
			{
			dp.disk_type = FFS;
			continue;
			}
	
		if (!strcmp( *arg_list, "-q" ) )
			{
			quick_format = 1;	
			continue;
			}

		if (!strcmp( *arg_list, "-nv" ) )
			{
			no_verify = 1;
			continue;
			}

		if (!strcmp( *arg_list, "-t" ) )
			{
			enter_tracks = 1;
			continue;
			}

		if (!strlen( volname ) )
			{
			strcpy( volname, *arg_list );
			continue;
			}

		/*
		  If we get here, our illustrious user has entered a
		  bad option. We will quit in case this was a typo.
		*/
		df_exit( EBADOPT, *arg_list );
		}

	/*
	  Assign a default volume name of "empty" if none was specified.
	*/
	if ( !strlen( volname ) )
		strcpy( volname, "empty" );
	}




/*-----------------------------------------------------------------------------
------------------------------------------------------------------------------*/
main( argc, argv )
int	argc;
char	**argv;

	{
	struct 	MsgPort		*CreatePort();
	struct	IOStdReq	*CreateStdIO();
	char			*AllocMem();
	long			OpenDevice();
	int			i;

	/* 
	  Say hello to joe user, and get our invocation name for later
	  use.
	*/
	printf( "%s", Greeting );
	my_name = *argv;

	/*
	  Make sure we got enough arguments..
	*/
	if ( argc < 2 )
		{
		df_exit( ENOARGS );
		}

	parse_args( ++argv );



	if (! (diskport = CreatePort( 0L, 0L )) )
		{
		df_exit( ENOPORT );
		}

	if (! (diskreq = CreateStdIO( diskport )) )
		{
		df_exit( ENOIOBLK );
		}


	if (! get_drive_stats( device ) )
		{
		df_exit( EBADDEV );
		}

	if ( (doe = OpenDevice( dp.driver, (long) dp.unit, diskreq, 0L ) ) )
		{
		df_exit( ENOOPEN );
		}

	dev_open = 1;


	/* 
	  Create the buffer used to hold information to write to
	  the disk during a format, and used in reads.
	*/
	if ( !(dsk_buf	= AllocMem( (long) dp.track_size,
				    MEMF_CHIP | MEMF_PUBLIC | MEMF_CLEAR ) ) )
		df_exit( ENOMEM );



	/*
	  We're ready to go. Make sure the disk is inserted if it is a floppy,
	  also give the user a chance to bail out.
	*/
	printf( "Insert disk in drive %s. Press <enter> to continue.", device );
	getchar();

	/*
	  Get the type of the drive...
	*/
	get_drive_type();


	/*
	  Initialize the bad_track_list.
	*/
	init_bad_track_list();

	/*
	  Ask the user to enter their own bad tracks (perhaps found
	  by another test). This part is optional - we do our own
	  format and verify, and add any new bad tracks to the list.
	*/
	if ( enter_tracks || update )
		get_bad_tracks();


	printf( "  Disk Statistics:\n" );
	printf( "\troot  key:  \t%ld\n", dp.root_key );
	printf( "\tboot  key:  \t%ld\n", dp.boot_key );
	printf( "\tlower key: \t%ld\n", dp.lower_key );
	printf( "\tupper key: \t%ld\n", dp.upper_key );

	/* 
	  If the user just wants to update the bad blocks (-u), we will
	  skip the rest of this junk and go straigh to update.
	*/
	if ( update )
		{
		update_bad_blocks();
		df_exit( 0 );
		}

	/*
	  If we just want a list of bad blocks, print it and quit...
	*/
	if ( report )
		{
		print_bad_blocks();
		df_exit( 0 );
		}


	/*
	  Format the disk...
	*/
	if ( dformat || quick_format || no_verify )
		format_disk();

	df_exit( 0 );
	}
	
		

