//
// (C) 2004 Mike Brent aka Tursi aka HarmlessLion.com
// This software is provided AS-IS. No warranty
// express or implied is provided.
//
// This notice defines the entire license for this code.
// All rights not explicity granted here are reserved by the
// author.
//
// You may redistribute this software provided the original
// archive is UNCHANGED and a link back to my web page,
// http://harmlesslion.com, is provided as the author's site.
// It is acceptable to link directly to a subpage at harmlesslion.com
// provided that page offers a URL for that purpose
//
// Source code, if available, is provided for educational purposes
// only. You are welcome to read it, learn from it, mock
// it, and hack it up - for your own use only.
//
// Please contact me before distributing derived works or
// ports so that we may work out terms. I don't mind people
// using my code but it's been outright stolen before. In all
// cases the code must maintain credit to the original author(s).
//
// -COMMERCIAL USE- Contact me first. I didn't make
// any money off it - why should you? ;) If you just learned
// something from this, then go ahead. If you just pinched
// a routine or two, let me know, I'll probably just ask
// for credit. If you want to derive a commercial tool
// or use large portions, we need to talk. ;)
//
// If this, itself, is a derived work from someone else's code,
// then their original copyrights and licenses are left intact
// and in full force.
//
// http://harmlesslion.com - visit the web page for contact info
//
//*****************************************************
//* Classic 99 - TI Emulator for Win32				  *
//* by M.Brent                                        *
//* Disk support routines                             *
//*****************************************************

// TODO: Rather than the open/close/open/close thrash it
// does now, open, perform operations on a buffer, then
// write the buffer back if necessary. This allows file
// pointer tracking and proper EOF, as well.

// Includes
#include <stdio.h>
#include <windows.h>
#include <FCNTL.H>
#include <io.h>
#include "tiemul.h"

// Defines
#define ERR_BADNAME			0				// This means the DSR was not found!
#define ERR_WRITEPROTECT	1
#define ERR_BADATTRIBUTE	2
#define ERR_ILLEGALOPERATION 3
#define ERR_BUFFERFULL		4
#define ERR_READPASTEOF		5
#define ERR_DEVICEERROR		6
#define ERR_FILEERROR		7

// Externs
extern Byte CPU[65536];						// Main CPU Memory
extern char diskpath[10][256];				// path to disk directories or images
extern char disktype[10];					// i=image, v=files with v9t9 header
											// t=files with TIFILES header,
											// r=raw program files without header
											// (load will attempt to autodetect)
extern struct DISKS *pMagicDisk;

void LoadOneImg(struct IMG *pImg, char *szFork);
bool TryLoadMagicImage(int PAB);

// Structures
struct {
	int LengthSectors;
	int FileType;
	int RecordsPerSector;
	int BytesInLastSector;
	int RecordLength;
	int NumberRecords;		// or sectors in a variable file
} FileInfo;

bool atEOF=false;	// Set true if last read hit EOF. Hacky, but should work in most cases
							// NOTE: only changed for reads!

int nNextVarRec=0;	// I dunno if this is right - autocount var records (1 file at a time this way)
							// Appears to be right (makes Major Tom work) - reading a variable record of
							// 0 reads the "next" record.
							// Note that Major Tom opens the file in update mode, but ignores the failure
							// so it works anyway ;)

///////////////////////////////////////////////////////////////////////
// Base entry for standard DSR call
///////////////////////////////////////////////////////////////////////
void do_dsrlnk()
{
	// performs file i/o using the PAB passed as per a normal
	// dsrlnk call. 
	// Added directory support to FIAD folders so ARC303 will work
	// address of length byte passed in CPU >8356
	
	int PAB;
	char filename[512];

	PAB = romword(0x8356)-14;			// base address of PAB in VDP RAM
	
	if ((VDP[PAB+13] <'0') || (VDP[PAB+13]>'9')) {
		setfileerror(PAB, ERR_BADATTRIBUTE);		// Bad drive = file error
		return;
	}

	buildfilename(filename, PAB);
	if (0==strlen(filename)) 
	{
		// Was this a LOAD request? If so, try the magic disk
		if (VDP[PAB] == 0x05) {
			if (TryLoadMagicImage(PAB)) {
				setnofileerror(PAB);
				return;
			}
		}

		setfileerror(PAB, ERR_FILEERROR);		// No filename = file error
		return;
	}

	switch (VDP[PAB]) 
	{
	case 0x00:		/* open */
		debug_write("Opening %s", filename);
		nNextVarRec=0;
		// All we do is verify that the file CAN be opened, since we don't
		// actually maintain open file handles
		FILE *fp;
		int drive;
		Byte type;
		int rpsector;

		// Get the file data
		drive=VDP[PAB+13] - '0';
		type=0;
		if (VDP[PAB+1]&0x10) {
			type|=0x80;	// variable length
			// max length is 254 - test that
			if (VDP[PAB+4] > 254) {
				setfileerror(PAB, ERR_BADATTRIBUTE);
				return;
			}
		}
		if (VDP[PAB+1]&0x08) type|=0x02;	// internal

		switch (VDP[PAB+1]&0x06) {	
		case 0x02:	// Output
			fp=fopen(filename,"wb");
			if (NULL == fp) break;

			if (VDP[PAB+4]==0) { VDP[PAB+4]=128; }	// user requested a default record length
			rpsector=256/(VDP[PAB+4]+((type&0x80)?1:0));

			// Write the appropriate header
			FileInfo.LengthSectors=0;
			FileInfo.FileType=type;
			FileInfo.RecordsPerSector=rpsector;
			FileInfo.BytesInLastSector=0;
			FileInfo.RecordLength=VDP[PAB+4];
			FileInfo.NumberRecords=0;
			WriteFileHeader(disktype[drive], fp, PAB);
			break;

		case 0x00:	// Update (TODO: this is probably wrong)
			/************/
			debug_write("Update Mode is not currently supported!");
			setfileerror(PAB, ERR_ILLEGALOPERATION);
			return;
			/************/

			GetFileInfo(filename);

			if (VDP[PAB+4]==0) VDP[PAB+4]=FileInfo.RecordLength;
			rpsector=256/(VDP[PAB+4]+((type&0x80)?1:0));

			fp=fopen(filename,"r+b");
			if (NULL == fp) break;

			if (FileInfo.RecordLength == 0) {
				// Write the appropriate header
				FileInfo.LengthSectors=0;
				FileInfo.FileType=type;
				FileInfo.RecordsPerSector=rpsector;
				FileInfo.BytesInLastSector=0;
				FileInfo.RecordLength=VDP[PAB+4];
				FileInfo.NumberRecords=0;
				WriteFileHeader(disktype[drive], fp, PAB);
			}
			break;

		default:	// Input, Append
			GetFileInfo(filename);
			if (VDP[PAB+4]==0) VDP[PAB+4]=FileInfo.RecordLength;
			{
				int size=(VDP[PAB+4]+((type&0x80)?1:0));
				if (size) {
					rpsector=256/size;
				} else {
					rpsector=0;
				}
			}

			fp=fopen(filename, "rb");
			atEOF=false;
			break;
		}

		if (NULL == fp) {
			// failed
			setfileerror(PAB, ERR_FILEERROR);		// file error
			return;
		} else {
			fclose(fp);

			GetFileInfo(filename);

			if (VDP[PAB+4] == 0) {			// We need to return the record length if it was set to zero
				VDP[PAB+4]=FileInfo.RecordLength;
			}

			// Verify the parameters as a last step before we OK it all :)
			if ((type)&&FileInfo.FileType != type) {
				debug_write("Incorrect file type: %d (real) vs %d (requested)", FileInfo.FileType, type);
				setfileerror(PAB, ERR_BADATTRIBUTE);		// Bad open attribute
				return;
			}

			if (FileInfo.RecordLength != VDP[PAB+4]) {
				debug_write("Record Length mismatch: %d (real) vs %d (requested)", FileInfo.RecordLength, VDP[PAB+4]);
				if (0 != type) {
					// if raw, try anyway
					setfileerror(PAB, ERR_BADATTRIBUTE);		// Bad open attribute
					return;
				}
			}

		}
		setnofileerror(PAB);
	break;

	case 0x01:		/* close */
		// We don't actually have to close anything right now - a real DSR might
		debug_write("Closing %s", filename);
		setnofileerror(PAB);
	break;

	case 0x02:		/* read */
		read(filename, PAB);
	break;

	case 0x03:		/* write */
		write(filename, PAB);
	break;

	case 0x04:		/* rewind */	
		/* TODO: for a relative file, use the record specified in bytes 6/7 */
		/* So, for now, there's really nothing to do */
		debug_write("Rewind file...");
		break;
			
	case 0x05:		/* load */
		load(filename, PAB);
	break;
	
	case 0x06:		/* save */
		save(filename, PAB);
	break;

	case 0x09:		/* status */
		// 
		// Currently we only return some of the bits
		// and EOF is hacked
		// >80: File not found
		// >40: File is write protected (not used here)
		// >20: Not used
		// >10: Internal
		// >08: Program
		// >04: Variable
		// >02: Memory full (used to check free space on disk - not used here)
		// >01: EOF
		{
			HANDLE tmp=CreateFile(filename, GENERIC_READ, FILE_SHARE_READ|FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
			int nOut;
			if (INVALID_HANDLE_VALUE == tmp) {
				VDP[PAB+8]=0x80;
				break;
			}
			CloseHandle(tmp);
			GetFileInfo(filename);
			nOut=0;

			// Is it too hard to use ONE set of flags?
			if (FileInfo.FileType & 0x80) nOut|=0x04;	// Variable
			if (FileInfo.FileType & 0x02) nOut|=0x10;	// Internal
			if (FileInfo.FileType & 0x01) nOut|=0x08;	// Program
			if (atEOF) nOut|=0x01;	// Hacky hacky hacky - and may fail on multiple open files
			
			VDP[PAB+8]=nOut&0xff;
			break;
		}


	// 0x07 - DELETE (file - I don't wish to support this anyway)
	// 0x08 - SCRATCH (record - not technically hard, but trickier without state)
	default:
		// bad opcode. Boy, this DSR sucks ;)
		// set return byte in PAB and exit
		debug_write("unsupported DSRLNK opcode %d on file %s", VDP[PAB], filename);
		setfileerror(PAB, ERR_BADATTRIBUTE);		// Bad open attribute
	break;
	}
}

///////////////////////////////////////////////////////////////////////
// Load a PROGRAM image file
///////////////////////////////////////////////////////////////////////
bool TryLoadMagicImage(int PAB) {
	// This function is called if the file's not on the disk, we
	// instead will try to read it from the resources
	// Resources are assumed to have no header
	char szFile[64];
	struct DISKS *pDsk;
	struct IMG tImg;
	int len, buffer, max_bytes;

	if (NULL == pMagicDisk) {
		return false;
	}

	// We need to build *just* the filename
	len=VDP[PAB+9];									// Filename length including 'DSKx.'
	buffer = ((VDP[PAB+2]<<8) | VDP[PAB+3])&0x3fff;	// VDP buffer
	max_bytes=(VDP[PAB+6]<<8) | VDP[PAB+7];			// maximum bytes to read

	if (len>63) return false;
	if (buffer > 0xffff) return false;
	if (max_bytes > 0x4000) return false;	// note: max image 8k!

	memcpy(szFile, &VDP[PAB+10], len);
	szFile[len]='\0';

	pDsk=pMagicDisk;

	while (pDsk->dwImg) {
		if (!strcmp(pDsk->szName, szFile)) {
			// Load this file
			tImg.dwImg=pDsk->dwImg;
			tImg.nType=TYPE_VDP;
			tImg.nLoadAddr=buffer;
			tImg.nLength=max_bytes;
			LoadOneImg(&tImg, "DISKFILES");
			return true;
		}
		pDsk++;
	}

	return false;
}

void load(char *filename, int PAB) 
{
	int buffer, max_bytes;
	FILE *fp;
	Byte drive;
	char x, *xp;

	drive=VDP[PAB+13] - '0';						// drive number
	buffer = ((VDP[PAB+2]<<8) | VDP[PAB+3])&0x3fff;	// VDP buffer
	max_bytes=(VDP[PAB+6]<<8) | VDP[PAB+7];			// maximum bytes to read

	if (disktype[drive] == 'i') 
	{
		if (TryLoadMagicImage(PAB)) {
			setnofileerror(PAB);
			return;
		}

		debug_write("loading on image disk %d not yet supported", drive);
		setfileerror(PAB, ERR_ILLEGALOPERATION);
		return;
	}

	fp=fopen(filename, "rb");
	if (NULL == fp)
	{
		if (TryLoadMagicImage(PAB)) {
			setnofileerror(PAB);
			return;
		}

		debug_write("cant load 0x%X bytes drive %d file %s", max_bytes, drive, filename);
		// couldn't open the file
		setfileerror(PAB, ERR_FILEERROR);
		return;
	}

	xp=filename+strlen(filename)-1;
	while ((xp>filename)&&(*xp != '\\')) xp--;
	if (*xp=='\\') xp++;
	x=detectfiletype(fp, xp);

	switch (x) {
	case 'v':
	case 't':
		fclose(fp);
		GetFileInfo(filename);
		if (0 == (FileInfo.FileType & 0x01)) {
			// not a PROGRAM image file
			debug_write("%s is not a PROGRAM file", filename);
			setfileerror(PAB, ERR_BADATTRIBUTE);
			return;
		}
		fp=fopen(filename, "rb");
		fseek(fp, 128, SEEK_SET);
		break;
	}

	if (xb) {		// We'll only do this funny test under XB
		// XB first tries to load as a PROGRAM image file with the
		// maximum available VDP RAM. If that fails with code 0x60,
		// it tries again as the proper DIS/FIX254
		// (The type checking above makes this largely obsolete)
		int fPos;

		fPos=ftell(fp);
		fseek(fp, -1, SEEK_END);
		// Work backwards to ignore any padding
		while (0 == fgetc(fp)) {
			fseek(fp, -2, SEEK_CUR);
		}

		if ((ftell(fp)-fPos) > max_bytes) {
			// Likely XB trying to load a non-program file
			fclose(fp);
			debug_write("Returning error to XB for non-PROGRAM image load");
			setfileerror(PAB, ERR_BADATTRIBUTE);
			return;
		}
		fseek(fp, fPos, SEEK_SET);
	}

	max_bytes = fread(&VDP[buffer], 1, max_bytes, fp);
	debug_write("loading 0x%X bytes drive %d file %s", max_bytes, drive, filename);

	setnofileerror(PAB);

	fclose(fp);										// all done
}

///////////////////////////////////////////////////////////////////////
// Read from a file
// The open command will eventually check this as much as possible
// Fixed records are obvious. Variable length records are prefixed
// with a byte that indicates how many bytes are in this record. It's
// all padded to 256 byte blocks. If it won't fit, the space in the
// rest of the sector is wasted (and 0xff marks it)
// DF80 data read from a raw file will attempt Windows text file translation
///////////////////////////////////////////////////////////////////////
void read(char *filename, int PAB) 
{
	int buffer, record_number, save_rec, record_length;
	int nLen, outLen, sector;
	FILE *fp;
	Byte drive;
	char x, *xp;
	int variable;
	char tmpbuf[256];

	drive=VDP[PAB+13] - '0';						// drive number
	buffer = ((VDP[PAB+2]<<8) | VDP[PAB+3])&0x3fff;	// VDP buffer
	record_number=(VDP[PAB+6]<<8) | VDP[PAB+7];		// which record to read
	if (record_number == 0) {
		record_number=nNextVarRec;
	}
	save_rec=record_number;
	record_length=VDP[PAB+4];						// length of each record
	outLen=0xff;

	atEOF=false;
	
	if (disktype[drive] == 'i') 
	{
		debug_write("reading on image disk %d not yet supported", drive);
		setfileerror(PAB, ERR_ILLEGALOPERATION);
		return;
	}

	fp=fopen(filename, "rb");
	if (NULL == fp)
	{
		debug_write("cant read from drive %d file %s", drive, filename);
		// couldn't open the file
		setfileerror(PAB, ERR_FILEERROR);
		return;
	}

	xp=filename+strlen(filename)-1;
	while ((xp>filename)&&(*xp != '\\')) xp--;
	if (*xp=='\\') xp++;
	x=detectfiletype(fp, xp);

	switch (x) {
	case 'v':
	case 't':
		fclose(fp);
		GetFileInfo(filename);
		variable=(FileInfo.FileType & 0x80);
		fp=fopen(filename, "rb");
		fseek(fp, 128, SEEK_SET);
		break;

	default:
		variable=detectfixedorvar(fp, record_length);
		//variable=(VDP[PAB+1]&0x10)?1:0;
	}

	// Seek to the desired record
	sector=256;
	while (record_number) {
		if (variable) {
			// variable length records
			nLen=fgetc(fp);
			sector--;
			if (nLen==0xff) {
				fread(tmpbuf, 1, sector, fp);
				sector=256;
			} else {
				fread(tmpbuf, 1, nLen, fp);
				sector-=nLen;
				record_number--;
			}
		} else {
			// fixed length records
			if (sector<record_length) {
				if ((x != 'r') || (record_length != 80)) {
					// for raw files, just pretend. This helps txt files read as DF80 files
					fread(tmpbuf, 1, sector, fp);
				}
				sector=256;
			} else {
				fread(tmpbuf, 1, record_length, fp);
				sector-=record_length;
				record_number--;
			}
			if ((x == 'r') && (record_length == 80)) {
				// remove cr/lf at the end of a DOS text file, or just lf for unix
				int c=fgetc(fp);
				if (c == 0x0d) {	// if we got a CR (DOS/Windows)
					c=fgetc(fp);	// get the LF
				} 
				if (c != 0x0a) {	// not an LF?
					ungetc(c, fp);	// oops, not what we expected
				}
			}
		}
	}

	// Check for end of sector
	if (variable) {
		// TODO: text file translation for DV80 files
		outLen=fgetc(fp);
		sector--;
		if (outLen==0xff) {
			fread(tmpbuf, 1, sector, fp);
			sector=256;
			outLen=fgetc(fp);
		}
	} else {
		if (sector<record_length) {
			if ((x != 'r') || (record_length != 80)) {
				// for raw files, just pretend. This helps txt files read as DF80 files
				fread(tmpbuf, 1, sector, fp);
			}
			sector=256;
		}
	}
	
	if (feof(fp)) {
		debug_write("seek past end of file %s", filename);
		atEOF=true;
		setfileerror(PAB, ERR_READPASTEOF);
		fclose(fp);
		return;
	}

	// Now we're at the right place
	if (variable) {
		// variable length records
		// already have outLen from above
		if (outLen!=0xff) {
			VDP[PAB+5]=fread(&VDP[buffer], 1, outLen, fp);
			sector-=outLen;
			if (fgetc(fp)==0xff) {		// check for the end tag
				char tmp[256];
				fread(tmp, 1, sector, fp);
				sector=256;
				atEOF=(feof(fp)!=0);
			}
		} else {
			debug_write("EOF for %s", filename);
			atEOF=true;
			setfileerror(PAB, ERR_READPASTEOF);
			fclose(fp);
			return;
		}
	} else {
		// fixed length records
		if (sector>=record_length) {
			VDP[PAB+5]=outLen=fread(&VDP[buffer], 1, record_length, fp);
			atEOF=(feof(fp)!=0);
		} else {
			debug_write("EOF for %s", filename);
			setfileerror(PAB, ERR_READPASTEOF);
			atEOF=true;
			fclose(fp);
			return;
		}
	}

	debug_write("reading 0x%X bytes drive %d file %s (%s record %d)", outLen, drive, filename, variable?"Variable":"Fixed", save_rec);

	save_rec++;
	nNextVarRec=save_rec;
	VDP[PAB+6]=save_rec>>8;
	VDP[PAB+7]=save_rec&0xff;

	setnofileerror(PAB);

	fclose(fp);										// all done
}

///////////////////////////////////////////////////////////////////////
// Write to a file
// See Read for format details
///////////////////////////////////////////////////////////////////////
void write(char *filename, int PAB) 
{
	int buffer, record_number, record_length;
	int charcount;
	int sector, tmp;
	FILE *fp;
	Byte drive;
	char x;
	int variable;

	drive=VDP[PAB+13] - '0';						// drive number
	buffer = ((VDP[PAB+2]<<8) | VDP[PAB+3])&0x3fff;	// VDP buffer
	record_number=(VDP[PAB+6]<<8) | VDP[PAB+7];		// which record to write for update (TODO)
	record_length=VDP[PAB+4];						// length of each record
	charcount=VDP[PAB+5];							// Number of characters to actually output

	// Since we don't allow UPDATE yet, it's OUTPUT or APPEND - so we scan to the end
	
	if (disktype[drive] == 'i') 
	{
		debug_write("writing on image disk %d not yet supported", drive);
		setfileerror(PAB, ERR_ILLEGALOPERATION);
		return;
	}

	fp=fopen(filename, "a+b");
	if (NULL == fp)
	{
		debug_write("cant open for writing drive %d file %s", drive, filename);
		// couldn't open the file
		setfileerror(PAB, ERR_FILEERROR);
		return;
	}

	fseek(fp, 0, SEEK_END);
	tmp=ftell(fp);
	fseek(fp, 0, SEEK_SET);

	x=detectfiletype(fp, filename);
	switch (x) {
	case 'v':
	case 't':
		tmp-=128;	// lose header
		fclose(fp);
		GetFileInfo(filename);
		variable=(FileInfo.FileType & 0x80);
		fp=fopen(filename, "a+b");
		break;

	default:
		debug_write("Warning: Writing to RAW files won't likely work.");
		variable=detectfixedorvar(fp, record_length);
	}

	// Seek to the end
	if (tmp>1) {
		if (variable) {
			// Need to remove the last byte
			fclose(fp);
			int fh=_open(filename, _O_RDWR, 0);
			if (-1 == fh) {
				debug_write("Couldn't truncate variable file %s", filename);
				setfileerror(PAB, ERR_FILEERROR);
				return;
			}
			_chsize(fh, _filelength( fh )-1);
			_close(fh);
			fp=fopen(filename, "a+b");
		}
	}
	fseek(fp, 0, SEEK_END);		// not necessary for append
	tmp=ftell(fp);
	if ((x=='v')||(x=='t')) tmp-=128;
	sector=256-(tmp%256);
	if (sector == 0) sector=256;
	if (variable) {
		if (sector < charcount+2) {
			// pad the sector out
			fputc(0xff,fp);
			sector--;
			while (sector) {
				fputc(0,fp);
				sector--;
			}
			sector=256;
			FileInfo.LengthSectors++;
			FileInfo.NumberRecords++;
		}
		fputc(charcount,fp);
		sector--;
		fwrite(&VDP[buffer], 1, charcount, fp);
		sector-=charcount;
		fputc(0xff,fp);
		sector--;
		FileInfo.BytesInLastSector=256-sector;
	} else {
		if (sector < record_length) {
			// pad the sector out
			while (sector) {
				fputc(0,fp);
				sector--;
			}
			sector=256;
			FileInfo.LengthSectors++;
		}
		fwrite(&VDP[buffer], 1, charcount, fp);
		record_length-=charcount;
		sector-=charcount;
		while (record_length) {
			fputc(0,fp);
			record_length--;
			sector--;
		}
		FileInfo.NumberRecords++;
		FileInfo.BytesInLastSector=256-sector;
		while (FileInfo.BytesInLastSector > 255) FileInfo.BytesInLastSector-=256;
	}
	fclose(fp);

	// Fix header
	switch (x) {
	case 'v':
	case 't':
		fp=fopen(filename, "r+b");
		if (NULL != fp) {
			WriteFileHeader(x, fp, PAB);
			fclose(fp);
		}
		break;
	}

	debug_write("writing 0x%X bytes drive %d file %s (%s)", charcount, drive, filename, variable?"Variable":"Fixed");

	setnofileerror(PAB);
}


///////////////////////////////////////////////////////////////////////////////
// Save a PROGRAM image file
///////////////////////////////////////////////////////////////////////////////
void save(char *filename, int PAB)
{
	int buffer, max_bytes;
	FILE *fp;
	Byte drive;
	
	// Execute a SAVE opcode (Program image)
	
	drive=VDP[PAB+13] - '0';	// drive number
	buffer = ((VDP[PAB+2]<<8) | VDP[PAB+3])&0x3fff;	// VDP buffer
	max_bytes=(VDP[PAB+6]<<8) | VDP[PAB+7];			// maximum bytes to read

	debug_write("saving 0x%X bytes file %s", max_bytes, filename);

	if (disktype[drive] == 'i') 
	{
		debug_write("saving on image disks not supported");
		setfileerror(PAB, ERR_ILLEGALOPERATION);
		return;
	}

	fp=fopen(filename, "wb");
	if (NULL == fp)
	{
		// couldn't open the file
		debug_write("cant open for writing");
		setfileerror(PAB, ERR_FILEERROR);
		return;
	}

	// Write the appropriate header
	FileInfo.LengthSectors=(max_bytes+255)/256;
	FileInfo.FileType=0x01;						// PROGRAM
	FileInfo.RecordsPerSector=0xff;
	FileInfo.BytesInLastSector=max_bytes&0xff;
	FileInfo.RecordLength=1;
	FileInfo.NumberRecords=max_bytes;
	WriteFileHeader(disktype[drive], fp, PAB);

	if (fwrite(VDP+buffer, 1, max_bytes, fp) == (unsigned int)max_bytes)
	{
		setnofileerror(PAB);
	}
	else
	{
		setfileerror(PAB, ERR_FILEERROR);
	}

	fclose(fp);										// all done
}


#define set_EQ  (ST|=0x2000)
#define reset_EQ  (ST&=0xdfff)

/////////////////////////////////////////////////////////////////////////
// Set the error bit in the PAB
/////////////////////////////////////////////////////////////////////////
void setfileerror(int PAB, Byte errcode) 
{
//	VDP[PAB+1] &= 0x1f;			// no errors
	VDP[PAB+1] |= errcode<<5;	// file error
	// Only set COND on non-existant DSR
}

/////////////////////////////////////////////////////////////////////////
// Set no error in the PAB
/////////////////////////////////////////////////////////////////////////
void setnofileerror(int PAB) 
{
	// It's up to the caller to clear the bits, not the DSR
//	VDP[PAB+1] &= 0x1f;		// no errors
	//wcpubyte(0x837c, rcpubyte(0x837c) & 0xdf);		// clear COND bit
	// Only set COND on non-existant DSR
}

/////////////////////////////////////////////////////////////////////////
// Assemble a filename to find the file
/////////////////////////////////////////////////////////////////////////
void buildfilename(char *filename, int PAB) 
{
	char *p;
	int idx;
	Byte len, drive;

	len=VDP[PAB+9] - 5;								// Filename length minus 'DSK1.' bit
	drive=VDP[PAB+13] - '0';						// drive number
  
	if (drive<1 || drive>9 || len<1 || 0==strlen(&diskpath[drive][0])) 
	{
		strcpy(filename, "");
		return;
	}

	p = (char *) stpcpy(filename, &diskpath[drive][0]); // points to end
	for (idx=0; idx<=len; idx++) 
	{												// search end, convert name 
		if (idx==len || VDP[PAB+15+idx]==' ') 
		{
			*p = 0;
			break;
		} else 
		{
			if (VDP[PAB+15+idx] == '/')
			{
				*p++ = '_';
			}
		    else
			{
				*p++ = tolower(VDP[PAB+15+idx]);
			}
		}
	}
}

/////////////////////////////////////////////////////////////////////////
// Perform low level disk call
/////////////////////////////////////////////////////////////////////////
void do_sbrlnk() 
{
	// performs low level disk i/o using the PAB passed as per a normal
	// dsrlnk call.

	// address of length byte passed in CPU >8356

	int PAB;
	Byte drive;
	int sect;
	int opcode;
	int buffer = romword(0x834e)&0x3fff;	// VDP buffer for sector
	Byte n = rcpubyte(0x834c);

	opcode = (PC & 0xff)>>1;
	PAB = romword(0x8356);					// base address of PAB in VDP RAM
	drive = rcpubyte(0x834c);
	sect = romword(0x8350);

	if (drive<1 || drive>9 || 0==strlen(&diskpath[drive][0])) 
	{
	  debug_write("SBRLNK call to invalid drive %d", drive);
	  wcpubyte(0x8350, 0x06);	/* hardware error */
	}

	switch (opcode) 
	{
	// This function is used for low-level sector IO
	case 0x10: 				/* sector read/write */
		debug_write("%sing drive %d sector %d", rcpubyte(0x834d) ? "read" : "writ", drive, sect);
		if (rcpubyte(0x834d)==0) 
		{
			wcpubyte(0x8350, 0x34);	/* write protected */
		} else 
		{
			read_sect(drive, sect, (char*)&VDP[buffer]);
		}
	break;

	// This function reads a file x sectors at a time
	case 0x14: {				/* File input */ 
		int nsec;
		Word infoptr;
		Word databuf;
		char szFile[12], szFile2[128];
		int idx, start, offset;
		FILE *fp;

		nsec=rcpubyte(0x834d);
		strcpy(szFile, "");
		idx=0;
		while ((idx<10)&&(VDP[buffer]!=' ')) {
			szFile[strlen(szFile)+1]='\0';
			szFile[strlen(szFile)]=VDP[buffer++];
			idx++;
		}
		sprintf(szFile2, "%s%s", &diskpath[drive][0], szFile);
		infoptr=0x8300 + rcpubyte(0x8350);
		databuf=romword(infoptr);
		start=romword(infoptr+2);

		if (nsec == 0) {
			// Get info request (sector 0)
			// (All bytes in CPU RAM)
			// infoptr+4 = file type
			// infoptr+5 = records/sector
			// infoptr+6 = EOF offset
			// infoptr+7 = record length
			// infoptr+8 = number records
			GetFileInfo(szFile2);
			wcpubyte(infoptr+4, FileInfo.FileType);
			wcpubyte(infoptr+5, FileInfo.RecordsPerSector);
			wcpubyte(infoptr+6, FileInfo.BytesInLastSector);
			wcpubyte(infoptr+7, FileInfo.RecordLength);
			wcpubyte(infoptr+8, FileInfo.NumberRecords);
			wcpubyte(0x8350, 0x0);		// no error
			debug_write("Information request on file %s (Type >%02x, Records Per Sector %d, EOF Offset %d, Record Length %d, Number Records %d)", szFile2, 
				FileInfo.FileType, FileInfo.RecordsPerSector, FileInfo.BytesInLastSector, FileInfo.RecordLength, FileInfo.NumberRecords);
			break;
		}

		debug_write("Reading drive %d file %s sector %d-%d to VDP %04x", drive, szFile2, start, start+nsec-1, databuf);

		// Read the requested sectors from the file
		fp=fopen(szFile2, "rb");
		
		if (NULL == fp) {
			wcpubyte(0x8350, 0xff);
			break;
		}

		switch (detectfiletype(fp, szFile2)) {
		case 'r': offset=0; break;
		case 'v':
		case 't': offset=128; break;
		}

		fseek(fp, start*256+offset, SEEK_SET);
		wcpubyte(0x834d, (char)fread(&VDP[databuf], 256, nsec, fp));

		wcpubyte(0x8350, 0);

		fclose(fp);

		break;
		}

	// This function Writes a file x sectors at a time
	case 0x15: {			/* write a sector to a file */
		int nsec;
		Word infoptr;
		Word databuf;
		char szFile[12], szFile2[128];
		int idx, start, offset;
		FILE *fp;

		nsec=rcpubyte(0x834d);
		strcpy(szFile, "");
		idx=0;
		while ((idx<10)&&(VDP[buffer]!=' ')) {
			szFile[strlen(szFile)+1]='\0';
			szFile[strlen(szFile)]=VDP[buffer++];
			idx++;
		}
		sprintf(szFile2, "%s%s", &diskpath[drive][0], szFile);
		infoptr=0x8300 + rcpubyte(0x8350);
		databuf=romword(infoptr);
		start=romword(infoptr+2);

		if (nsec == 0) {
			// Create File
			// (All bytes in CPU RAM)
			// infoptr+4 = file type
			// infoptr+5 = records/sector
			// infoptr+6 = EOF offset
			// infoptr+7 = record length
			// infoptr+8 = number records
			FileInfo.FileType = rcpubyte(infoptr+4);
			FileInfo.RecordsPerSector=rcpubyte(infoptr+5);
			FileInfo.BytesInLastSector=rcpubyte(infoptr+6);
			FileInfo.RecordLength=rcpubyte(infoptr+7);
			FileInfo.NumberRecords=rcpubyte(infoptr+8);

			fp=fopen(szFile2, "wb");
			if (NULL == fp) {
				wcpubyte(0x8350, 0xff);
				break;
			}

			WriteFileHeader(disktype[drive], fp, PAB);
			fclose(fp);

			wcpubyte(0x8350, 0x0);		// no error
			debug_write("Low-level created file %s (Type >%02x, Records Per Sector %d, EOF Offset %d, Record Length %d, Number Records %d)", szFile2, 
				FileInfo.FileType, FileInfo.RecordsPerSector, FileInfo.BytesInLastSector, FileInfo.RecordLength, FileInfo.NumberRecords);
			break;
		}

		debug_write("Writing drive %d file %s sector %d-%d from VDP %04x", drive, szFile2, start, start+nsec-1, databuf);

		// Write the requested sectors to the file
		// TODO: we currently assume APPENDING - which may be wrong!
		fp=fopen(szFile2, "a+b");
		
		if (NULL == fp) {
			wcpubyte(0x8350, 0xff);
			break;
		}

		switch (detectfiletype(fp, szFile2)) {
		case 'r': offset=0; break;
		case 'v':
		case 't': offset=128; break;
		}

		// This seek will do nothing at all in append mode
		fseek(fp, start*256+offset, SEEK_SET);

		// Write the sectors
		wcpubyte(0x834d, (char)fwrite(&VDP[databuf], 256, nsec, fp));
		wcpubyte(0x8350, 0);

		fclose(fp);

		break;
		}
	
	case 0x16: 				/* call files */
		if (n>0 && n<=16) 
		{
			do_files(n);
			wcpubyte(0x8350, 0);		/* no error */
		} else	  
		{
			wcpubyte(0x8350, 0xff);		/* error */
		}
	break;
	
	case 0x11:				/* format disk - no support planned */
	case 0x12:				/* file protection - no support planned */
	case 0x13:				/* rename file - no support planned */
	default:
		debug_write("unsupported SBRLNK opcode 0x%x", opcode);
		wcpubyte(0x8350, 0x06);			/* hardware error */
	break;
	}
}

/////////////////////////////////////////////////////////////////////////
// Read a sector (for directories and the like)
/////////////////////////////////////////////////////////////////////////
void read_sect(Byte drive, int sect, char *buffer) 
{
	FILE *fp;

	switch (tolower(disktype[drive])) 

	{
	case 'i':
		fp = fopen(&diskpath[drive][0], "rb");
		if (fp) 
		{
			fseek(fp, sect<<8, SEEK_SET);
			if (fread(buffer, 1, 256, fp)==256)
			{
				wcpubyte(0x8350, 0);			/* no error */
			}
			else
			{
				wcpubyte(0x8350, 0x06);			/* hardware error */
			}
			fclose(fp);
		} else 
		{
			debug_write("cant open %s", &diskpath[drive][0]);
			memset(buffer, 0, 256);
			wcpubyte(0x8350, 0x06);				/* hardware error */
		}
	break;

	case 'v':								/* v9t9: simulated directory sector access */
	case 't':								/* TIFILES: simulated directory sector access */
	case 'r':								/* raw: simulated directory sector access */
		wcpubyte(0x8350, 0);				/* no error */
		memset(buffer, 0, 256);				/* init buffer */
		if (sect==0) 
		{									/* sector 0 */
			char *p;
			if (strlen(&diskpath[drive][0]) < 10) 
			{
				p = (char *) stpcpy(buffer, &diskpath[drive][0]); /* points to end */
				while (p < buffer+10)
				{
					*p++ = ' ';
				}
			} else 
			{
				strcpy(buffer, &diskpath[drive][0] + strlen(&diskpath[drive][0]) - 10);
			}
			p = buffer+0x0a;
			*p++ = 0x05;
			*p++ = (unsigned char)0xa0;
			*p++ = 0x12;
			*p++ = 'D';
			*p++ = 'S';
			*p++ = 'K';
			*p++ = 'P';
			*p++ = 0x28;
			*p++ = 0x02;
			*p++ = 0x02;
			for (p=buffer+0x38; p<buffer+256; p++)
			{
				*p = (unsigned char)0xff;
			}
		} else 
		{			/* other sector */
			int n;
			int idx;

			// get a directory - max of 127 files
			char Filename[127][256];
			HANDLE fsrc;
			WIN32_FIND_DATA myDat;
			char srchbuf[256];

			n=0;
			sprintf(srchbuf, "%s*.*", &diskpath[drive][0]);
			fsrc=FindFirstFile(srchbuf, &myDat);
			if (INVALID_HANDLE_VALUE == fsrc) {
				n=0;
			} else {
				do {
					// Make uppercase
					_strupr(myDat.cFileName);
					// verify we can open it, ignore files we can't (dirs?)
					sprintf(srchbuf, "%s%s", &diskpath[drive][0], myDat.cFileName);
					fp=fopen(srchbuf, "rb");
					if (fp) {
						strcpy(&Filename[n++][0], myDat.cFileName);
						fclose(fp);
						if (n>126) break;
					}
				} while (FindNextFile(fsrc, &myDat));
				FindClose(fsrc);
			}

			if (sect==1) 
			{
				int i;
				for (i=0; i<n && i<127; i++)
				{
					buffer[2*i+1] = i+2;
				}
			}
			else 
			{
				if (n>0 && sect>=2 && sect<n+2) 
				{
					char *p, *q;
					int len;
					char tifbuf[128];
	
					// malloc space for filename
					p = (char *) malloc(strlen(&diskpath[drive][0]) + strlen(&Filename[sect-2][0]) + 1);
					strcpy(p, &diskpath[drive][0]);
					strcat(p, &Filename[sect-2][0]);
					fp=fopen(p, "rb");
					if (fp) {				// ignore failed opens (directories?)
						switch (detectfiletype(fp, &Filename[sect-2][0])) 
						{
							case 'v': 		/* v9t9: simulated directory sector access */
								fread(buffer, 1, 0x1b, fp); /* file information without link map */
								// the first characters should match the filename if this
								// is really a v9t9 file. If it's not, we'll fall through and
								// do the RAW processing instead
								idx=0;
								while ((idx<10)&&(*(buffer+idx)!=' ')) {
									if (toupper(*(buffer+idx))!=Filename[sect-2][idx]) {
										idx=99;
										break;
									}
									idx++;
								}
								if (idx != 99) {
									break;
								} 
								// else clear buffer then fall through
								memset(buffer, 0, 256);

							case 'r': 		/* raw: simulated directory sector access */
								// copy the filename and set q to point the end of the string
								strncpy(buffer, &Filename[sect-2][0], 10);
								if (strlen(&Filename[sect-2][0])>10) {
									q=buffer+10;
								} else {
									q=buffer+strlen(&Filename[sect-2][0]);
								}
								while (q < buffer+10)
								{
									*q++ = ' ';
								}
								fseek(fp, 0, SEEK_END);
								len = ftell(fp);
								buffer[0x0c] = 0x09;	/* always PROGRAM */
								buffer[0x0e] = (len+255) >> 16;
								buffer[0x0f] = (len+255) >> 8;
								buffer[0x10] = len & 0xff;
							break;

							case 't': 		/* TIFILES: simulated directory sector access */
								strncpy(buffer, &Filename[sect-2][0], 10);
								if (strlen(&Filename[sect-2][0])>10) {
									q=buffer+10;
								} else {
									q=buffer+strlen(&Filename[sect-2][0]);
								}
								while (q < buffer+10)
								{
									*q++ = ' ';
								}
								fread(tifbuf, 1, 128, fp); /* TIFILES header */
								q += 2;					/* start at offset 0x0c */
								*q++ = tifbuf[10];
								*q++ = tifbuf[11];
								*q++ = tifbuf[8];
								*q++ = tifbuf[9];
								*q++ = tifbuf[12];
								*q++ = tifbuf[13];
								*q++ = tifbuf[14];
								*q = tifbuf[15];
							break;
						} /* switch */
						fclose(fp);
					}
					free(p);
				} else 
				{
					debug_write("attempt to read not simulated sector");
					wcpubyte(0x8350, 0x06);	/* hardware error */
				}
			}
		}
	break;

	default:
		debug_write("sector access only implemented for images and v9t9");
		wcpubyte(0x8350, 0x06);				/* hardware error */
	} /* switch */
}


/////////////////////////////////////////////////////////////////////////
// Read (part of) a file from a disk image
/////////////////////////////////////////////////////////////////////////
#if 0
void read_image_file(int PAB, char *buffer, int offset, int len) 
{
	Byte bufs1[256], buffds[256], fn[10];
	Byte drive;
	Byte *p1, *p2, *p3;
	Word Start, Length;
	int found, fds, i;
	char temp[1024];

	drive=VDP[PAB+13] - '0';	// drive number
	read_sect(drive, 1, bufs1);
	p1 = stpncpy(fn, VDP+PAB+15, VDP[PAB+9]-5);
	while (p1<fn+10)				/* fill with spaces for comparing */
	{
		*p1++ = ' ';
	}
	p1=bufs1, p2=bufs1+256;
	found=0;
	
	do 
	{
		p = p1 + (((p2-p1)/2) & ~1);
		fds = (*p)<<8 | *(p+1);
		if (!fds) 
		{
			p2 = p;
			continue;
		}
		read_sect(drive, fds, buffds);
		if (0 == (cmp=strncmp(fn, buffds, 10))) 
		{
			found=1;
			break;
		} else 
		{
			if (cmp>0)
			{
				p1=p+2;
			}
			else
			{
				p2=p;
			}
		}
	} while (p1<p2);

	if (found) 
	{
		size = 256;
		Length = 0;
		Prog = buffds[12] & 1;
		Dis = (buffds[12] & 2) ? 0 : 1;
		Var = buffds[12] >> 7;
		Len = buffds[0x11];
		debug_write("Copying %d %s %s %s%s%s%d", o, argv[o], filename, Prog ? "P": " ", !Prog && Dis ? "D": " ", !Prog && Var ? "V": " ", Len);
		for (p3 = buffds+0x1c; *p3 | *(p3+1); p3 += 3) 
		{		/* all chunks */
			Start = *p3 | ((*(p3+1) & 0x0f) << 8);
			Length = (((*(p3+1) & 0xf0) >> 4) | (*(p3+2) << 4)) - Length;
			for (i=0; i<=Length; i++) 
			{ /* read every sector */
				read_sect(Start++, buffer);
				if (Prog && i==Length && (*(p3+3) | *(p3+4)) == 0)
				{
					size =  buffds[0x10] ?  buffds[0x10] : 256;
				}
				/* last sector of P-File: shorter */
				fwrite(buffer, size, 1, outfile);
			} /* for i */
		} /* for p3 */
	} /* found / for p */
}
#endif

///////////////////////////////////////////////////////////////////////
// Equivalent to CALL FILES(n)
// This is unnecessary as we don't use VDP RAM buffers. Since the
// RAMdisk gets away with it, so can we. Still, it's good to have this
// code to see how it's done
///////////////////////////////////////////////////////////////////////
void do_files(int /*n*/)				/* call files(n) */ 
{						
	return;

#if 0
	Byte *p;
	int adr = 0x4000 - n*518 - 534;		/* first address of disk data (FIXME: ignore previously allocated space) */

	wrword(0x8370, (adr-1) & 0x3fff);	/* last free byte in VDP RAM */
	p = &VDP[adr];
	*p++ = 0xaa;						/* ident */
	*p++ = 0x3f;						/* link (last entry) */
	*p++ = 0xff;
	*p++ = 0x11;						/* CRU page */
	*p++ = n;							/* max. number of files */
	for (; n; n--) 
	{
		*(p+6) = 0;						/* clear first byte of FDS to indicate unused buffer */
		p += 518;						/* next buffer */
	}
#endif
}

///////////////////////////////////////////////////////////////////////
// Fills in FileInfo
///////////////////////////////////////////////////////////////////////
void GetFileInfo(char *filename) {
	FILE *fp;
	Byte buf[256];
	char type;

	memset(&FileInfo, 0, sizeof(FileInfo));

	fp=fopen(filename, "rb");
	if (NULL == fp) return;

	type=detectfiletype(fp, filename);
	fread(buf, 256, 1, fp);
	fclose(fp);

	switch (type) {
	case 'v':
		FileInfo.LengthSectors=(buf[14]<<8)|buf[15];
		FileInfo.FileType=buf[12];
		FileInfo.RecordsPerSector=buf[13];
		FileInfo.BytesInLastSector=buf[16];
		FileInfo.RecordLength=buf[17];
		FileInfo.NumberRecords=buf[18]|(buf[19]<<8);
		break;

	case 't':
		FileInfo.LengthSectors=(buf[8]<<8)|buf[9];
		FileInfo.FileType=buf[10];
		FileInfo.RecordsPerSector=buf[11];
		FileInfo.BytesInLastSector=buf[12];
		FileInfo.RecordLength=buf[13];
		FileInfo.NumberRecords=buf[14]|(buf[15]<<8);
		break;
	}
}

///////////////////////////////////////////////////////////////////////
// Writes a header - note: moves file pointer - overwrites first 128
// bytes unless drive is set to RAW. Must only be called on files
// with WRITE access.
///////////////////////////////////////////////////////////////////////
void WriteFileHeader(char type, FILE *fp, int PAB) {
	fseek(fp, 0, SEEK_SET);
	
	if (type == 'r') return;

	if (type == 'v') 
	{	/* create v9t9 header */
		Byte h[128];							// header 
		Byte *p;

		memset(h, 0, 128);						// initialize 
		
		for (p=h; p < (h+VDP[PAB+9]-5); p++) 
		{
			*p = VDP[PAB+15+(p-h)];
		}
		while (p<h+10)
		{
			*p++ = ' ';							// fill with blanks 
		}

		h[12] = FileInfo.FileType;				// File type
		h[13] = FileInfo.RecordsPerSector;		// records/sector 
		h[14] = FileInfo.LengthSectors>>8;		// length in sectors HB 
		h[15] = FileInfo.LengthSectors&0xff;	// LB 
		h[16] = FileInfo.BytesInLastSector;		// # of bytes in last sector 
		h[17] = FileInfo.RecordLength;			// record length 
		h[18] = FileInfo.NumberRecords&0xff;	// # of records(FIX)/sectors(VAR) LB! 
		h[19] = FileInfo.NumberRecords>>8;		// HB 
		fwrite(h, 1, 128, fp);
	} else {
		/* create TIFILES header */
		Byte h[128];								// header

		memset(h, 0, 128);							// initialize
		h[0] = 7;
		h[1] = 'T';
		h[2] = 'I';
		h[3] = 'F';
		h[4] = 'I';
		h[5] = 'L';
		h[6] = 'E';
		h[7] = 'S';
		h[8] = FileInfo.LengthSectors>>8;			// length in sectors HB
		h[9] = FileInfo.LengthSectors&0xff;			// LB 
		h[10] = FileInfo.FileType;					// File type 
		h[11] = FileInfo.RecordsPerSector;			// records/sector
		h[12] = FileInfo.BytesInLastSector;			// # of bytes in last sector
		h[13] = FileInfo.RecordLength;				// record length 
		h[14] = FileInfo.NumberRecords&0xff;		// # of records(FIX)/sectors(VAR) LB!
		h[15] = FileInfo.NumberRecords>>8;			// HB 
		fwrite(h, 1, 128, fp);
	}
}

////////////////////////////////////////////////////////////////
// detectfiletype - passed a filename and open file at BOF, return v, t or r
// File pointer is reset to the beginning of the file
////////////////////////////////////////////////////////////////
char detectfiletype(FILE *fp, char *p) {
	char buffer[32];
	int idx;
	
	if (strrchr(p, '\\')) {
		p=strrchr(p, '\\')+1;
	}

	fread(buffer, 1, 0x1b, fp); /* file information without link map */
	fseek(fp, 0, SEEK_SET);

	// the first characters should match the filename if this
	// is really a v9t9 file. 
	idx=0;
	while ((idx<10)&&(*(buffer+idx)!=' ')) {
		if (toupper(*(buffer+idx))!=toupper(*(p+idx))) {
			idx=99;
			break;
		}
		idx++;
	}
	if (idx != 99) {
		debug_write("Detected %s as a V9T9 file", p);
		return 'v';
	} 

	// the first characters would be TIFILES if it's a TIFILES file
	if (0==strnicmp(buffer+1, "TIFILES", 7)) {
		debug_write("Detected %s as a TIFILES file", p);
		return 't';
	}

	// else it must be raw
	debug_write("Assuming %s is a RAW file", p);
	return 'r';
}

////////////////////////////////////////////////////////////////
// detectfixedorvar - analyzes the first sector and returns
// 0 for fixed and 1 for variable length records. If it can't
// tell it assumes fixed.
////////////////////////////////////////////////////////////////
int detectfixedorvar(FILE *fp, int reclength) {
	int fPos, x, len, left;
	unsigned char sector[256];

	fPos=ftell(fp);
	memset(sector, 0, 256);
	for (x=0; x<256; x++) {
		sector[x]=fgetc(fp);
	}
	fseek(fp, fPos, SEEK_SET);

	// see if the sector contains reasonable variable data
	x=0;
	left=256;
	while (left) {
		len=sector[x++];
		left--;
		if ((len==0xff)&&(left<=reclength)) {
			return 1;
		}
		if (len>left) {
			return 0;	// nonsense
		}
		x+=len;
		left-=len;
	}
	return 0;
}
