#ifndef CPM_DISK_H
#define CPM_DISK_H

/* see main.c for definition */

#ifdef __linux__
#include <sys/stat.h>
#endif

#ifdef __DJGPP__
#include <dirent.h>
#endif

/*
Disk errors
*/
#define errWRITEPROT 1
#define errSELECT 2

#define RW	(roVector & (1 << cDrive))

// Prints out a BDOS error
void _error(uint8 error) {
	_puts("\r\nBdos Err on ");
	_putcon('A' + cDrive);
	_puts(": ");
	switch (error) {
	case errWRITEPROT:
		_puts("R/O");
		break;
	case errSELECT:
		_puts("Select");
		break;
	default:
		_puts("\r\nCP/M ERR");
		break;
	}
	Status = _getch();
	_puts("\r\n");
	cDrive = oDrive = _RamRead(DSKByte) & 0x0f;
	Status = 2;
}

// Selects the disk to be used by the next disk function
int _SelectDisk(uint8 dr) {
	uint8 result = 0xff;
	uint8 disk[2] = { 'A', 0 };

	if (!dr || dr == '?') {
		dr = cDrive;	// This will set dr to defDisk in case no disk is passed
	} else {
		--dr;			// Called from BDOS, set dr back to 0=A: format
	}

	disk[0] += dr;
	if (_sys_select(&disk[0])) {
		loginVector = loginVector | (1 << (disk[0] - 'A'));
		result = 0x00;
	} else {
		_error(errSELECT);
	}

	return(result);
}

// Converts a FCB entry onto a host OS filename string
uint8 _FCBtoHostname(uint16 fcbaddr, uint8* filename) {
	uint8 addDot = TRUE;
	CPM_FCB* F = (CPM_FCB*)_RamSysAddr(fcbaddr);
	uint8 i = 0;
	uint8 unique = TRUE;
	uint8 c;

	if (F->dr && F->dr != '?') {
		*(filename++) = (F->dr - 1) + 'A';
	} else {
		*(filename++) = cDrive + 'A';
	}
	*(filename++) = FOLDERCHAR;

	*(filename++) = toupper(tohex(userCode));
	*(filename++) = FOLDERCHAR;

	if (F->dr != '?') {
		while (i < 8) {
			c = F->fn[i] & 0x7F;
#ifdef NOSLASH
			if (c == '/')
				c = '_';
#endif
			if (c > 32)
				*(filename++) = toupper(c);
			if (c == '?')
				unique = FALSE;
			++i;
		}
		i = 0;
		while (i < 3) {
			c = F->tp[i] & 0x7F;
			if (c > 32) {
				if (addDot) {
					addDot = FALSE;
					*(filename++) = '.';  // Only add the dot if there's an extension
				}
#ifdef NOSLASH
				if (c == '/')
					c = '_';
#endif
				*(filename++) = toupper(c);
			}
			if (c == '?')
				unique = FALSE;
			++i;
		}
	} else {
		for (i = 0; i < 8; ++i) {
			*(filename++) = '?';
		}
		*(filename++) = '.';
		for (i = 0; i < 3; ++i) {
			*(filename++) = '?';
		}
		unique = FALSE;
	}
	*filename = 0x00;

	return(unique);
}

// Convers a host OS filename string onto a FCB entry
void _HostnameToFCB(uint16 fcbaddr, uint8* filename) {
	CPM_FCB* F = (CPM_FCB*)_RamSysAddr(fcbaddr);
	uint8 i = 0;

	++filename;
	if (*filename == FOLDERCHAR) {	// Skips the drive and / if needed
		filename += 3;
	} else {
		--filename;
	}

	while (*filename != 0 && *filename != '.') {
		F->fn[i] = toupper(*filename);
		++filename;
		++i;
	}
	while (i < 8) {
		F->fn[i] = ' ';
		++i;
	}
	if (*filename == '.')
		++filename;
	i = 0;
	while (*filename != 0) {
		F->tp[i] = toupper(*filename);
		++filename;
		++i;
	}
	while (i < 3) {
		F->tp[i] = ' ';
		++i;
	}
}

// Converts a string name (AB.TXT) onto FCB name (AB      TXT)
void _HostnameToFCBname(uint8* from, uint8* to) {
	int i = 0;

	++from;
	if (*from == FOLDERCHAR) {	// Skips the drive and / if needed
		from += 3;
	} else {
		--from;
	}

	while (*from != 0 && *from != '.') {
		*to = toupper(*from);
		++to; ++from; ++i;
	}
	while (i < 8) {
		*to = ' ';
		++to;  ++i;
	}
	if (*from == '.')
		++from;
	i = 0;
	while (*from != 0) {
		*to = toupper(*from);
		++to; ++from; ++i;
	}
	while (i < 3) {
		*to = ' ';
		++to;  ++i;
	}
	*to = 0;
}

// Creates a fake directory entry for the current dmaAddr FCB
void _mockupDirEntry(void) {
	CPM_DIRENTRY* DE = (CPM_DIRENTRY*)_RamSysAddr(dmaAddr);
	uint8 blocks, i;

	for (i = 0; i < sizeof(CPM_DIRENTRY); ++i) {
		_RamWrite(dmaAddr + i, 0x00); // zero out directory entry
	}
	_HostnameToFCB(dmaAddr, (uint8*)findNextDirName);

	if (allUsers) {
		DE->dr = currFindUser; // set user code for return
	} else {
		DE->dr = userCode;
	}
	// does file fit in a single directory entry?
	if (fileExtents <= extentsPerDirEntry) {
		if (fileExtents) {
			DE->ex = (fileExtents - 1 + fileExtentsUsed) % (MaxEX + 1);
			DE->s2 = (fileExtents - 1 + fileExtentsUsed) / (MaxEX + 1);
			DE->rc = fileRecords - (BlkEX * (fileExtents - 1));
		}
		blocks = (fileRecords >> blockShift) + ((fileRecords & blockMask) ? 1 : 0);
		fileRecords = 0;
		fileExtents = 0;
		fileExtentsUsed = 0;
	} else { // no, max out the directory entry
		DE->ex = (extentsPerDirEntry - 1 + fileExtentsUsed) % (MaxEX + 1);
		DE->s2 = (extentsPerDirEntry - 1 + fileExtentsUsed) / (MaxEX + 1);
		DE->rc = BlkEX;
		blocks = numAllocBlocks < 256 ? 16 : 8;
		// update remaining records and extents for next call
		fileRecords -= BlkEX * extentsPerDirEntry;
		fileExtents -= extentsPerDirEntry;
		fileExtentsUsed += extentsPerDirEntry;
	}
	// phoney up an appropriate number of allocation blocks
	if (numAllocBlocks < 256) {
		for (i = 0; i < blocks; ++i) {
			DE->al[i] = (uint8)firstFreeAllocBlock++;
		}
	} else {
		for (i = 0; i < 2 * blocks; i += 2) {
			DE->al[i] = firstFreeAllocBlock & 0xFF;
			DE->al[i + 1] = firstFreeAllocBlock >> 8;
			++firstFreeAllocBlock;
		}
	}
}

// Matches a FCB name to a search pattern
uint8 match(uint8* fcbname, uint8* pattern) {
	uint8 result = 1;
	uint8 i;

	for (i = 0; i < 12; ++i) {
		if (*pattern == '?' || *pattern == *fcbname) {
			++pattern; ++fcbname;
			continue;
		} else {
			result = 0;
			break;
		}
	}
	return(result);
}

// Returns the size of a file
long _FileSize(uint16 fcbaddr) {
	CPM_FCB* F = (CPM_FCB*)_RamSysAddr(fcbaddr);
	long r, l = -1;

	if (!_SelectDisk(F->dr)) {
		_FCBtoHostname(fcbaddr, &filename[0]);
		l = _sys_filesize(filename);
		r = l % BlkSZ;
		if (r)
			l = l + BlkSZ - r;
	}
	return(l);
}

// Opens a file
uint8 _OpenFile(uint16 fcbaddr) {
	CPM_FCB* F = (CPM_FCB*)_RamSysAddr(fcbaddr);
	uint8 result = 0xff;
	long len;
	int32 i;

	if (!_SelectDisk(F->dr)) {
		_FCBtoHostname(fcbaddr, &filename[0]);
		if (_sys_openfile(&filename[0])) {

			len = _FileSize(fcbaddr) / BlkSZ;	// Compute the len on the file in blocks

			F->s1 = 0x00;
			F->s2 = 0x80;	// set unmodified flag


			F->rc = len > MaxRC ? MaxRC : (uint8)len;
			for (i = 0; i < 16; ++i)	// Clean up AL
				F->al[i] = 0x00;

			result = 0x00;
		}
	}
	return(result);
}

// Closes a file
uint8 _CloseFile(uint16 fcbaddr) {
	CPM_FCB* F = (CPM_FCB*)_RamSysAddr(fcbaddr);
	uint8 result = 0xff;

	if (!_SelectDisk(F->dr)) {
		if (!(F->s2 & 0x80)) {					// if file is modified
			if (!RW) {
				_FCBtoHostname(fcbaddr, &filename[0]);
				if (fcbaddr == BatchFCB)
					_Truncate((char*)filename, F->rc);	// Truncate $$$.SUB to F->rc CP/M records so SUBMIT.COM can work
				result = 0x00;
			} else {
				_error(errWRITEPROT);
			}
		} else {
			result = 0x00;
		}
	}
	return(result);
}

// Creates a file
uint8 _MakeFile(uint16 fcbaddr) {
	CPM_FCB* F = (CPM_FCB*)_RamSysAddr(fcbaddr);
	uint8 result = 0xff;
	uint8 i;

	if (!_SelectDisk(F->dr)) {
		if (!RW) {
			_FCBtoHostname(fcbaddr, &filename[0]);
			if (_sys_makefile(&filename[0])) {
				F->ex = 0x00;	// Makefile also initializes the FCB (file becomes "open")
				F->s1 = 0x00;
				F->s2 = 0x00;		// newly created files are already modified
				F->rc = 0x00;
				for (i = 0; i < 16; ++i)	// Clean up AL
					F->al[i] = 0x00;
				F->cr = 0x00;
				result = 0x00;
			}
		} else {
			_error(errWRITEPROT);
		}
	}
	return(result);
}

// Searches for the first directory file
uint8 _SearchFirst(uint16 fcbaddr, uint8 isdir) {
	CPM_FCB* F = (CPM_FCB*)_RamSysAddr(fcbaddr);
	uint8 result = 0xff;

	if (!_SelectDisk(F->dr)) {
		_FCBtoHostname(fcbaddr, &filename[0]);
		allUsers = F->dr == '?';
		allExtents = F->ex == '?';
		if (allUsers) {
			result = _findfirstallusers(isdir);
		} else {
			result = _findfirst(isdir);
		}
	}
	return(result);
}

// Searches for the next directory file
uint8 _SearchNext(uint16 fcbaddr, uint8 isdir) {
	CPM_FCB* F = (CPM_FCB*)_RamSysAddr(tmpFCB);
	uint8 result = 0xff;

	if (!_SelectDisk(F->dr)) {
		if (allUsers) {
			result = _findnextallusers(isdir);
		} else {
			result = _findnext(isdir);
		}
	}
	return(result);
}

// Deletes a file
uint8 _DeleteFile(uint16 fcbaddr) {
	CPM_FCB* F = (CPM_FCB*)_RamSysAddr(fcbaddr);
#if defined(USE_PUN) || defined(USE_LST)
	CPM_FCB* T = (CPM_FCB*)_RamSysAddr(tmpFCB);
#endif
	uint8 result = 0xff;
	uint8 deleted = 0xff;

	if (!_SelectDisk(F->dr)) {
		if (!RW) {
			result = _SearchFirst(fcbaddr, FALSE);	// FALSE = Does not create a fake dir entry when finding the file
			while (result != 0xff) {
#ifdef USE_PUN
				if (!strcmp((char*)T->fn, "PUN     TXT") && pun_open) {
					_sys_fclose(pun_dev);
					pun_open = FALSE;
				}
#endif
#ifdef USE_LST
				if (!strcmp((char*)T->fn, "LST     TXT") && lst_open) {
					_sys_fclose(lst_dev);
					lst_open = FALSE;
				}
#endif
				_FCBtoHostname(tmpFCB, &filename[0]);
				if (_sys_deletefile(&filename[0]))
					deleted = 0x00;
				result = _SearchFirst(fcbaddr, FALSE);	// FALSE = Does not create a fake dir entry when finding the file
			}
		} else {
			_error(errWRITEPROT);
		}
	}
	return(deleted);
}

// Renames a file
uint8 _RenameFile(uint16 fcbaddr) {
	CPM_FCB* F = (CPM_FCB*)_RamSysAddr(fcbaddr);
	uint8 result = 0xff;

	if (!_SelectDisk(F->dr)) {
		if (!RW) {
			_RamWrite(fcbaddr + 16, _RamRead(fcbaddr));	// Prevents rename from moving files among folders
			_FCBtoHostname(fcbaddr + 16, &newname[0]);
			_FCBtoHostname(fcbaddr, &filename[0]);
			if (_sys_renamefile(&filename[0], &newname[0]))
				result = 0x00;
		} else {
			_error(errWRITEPROT);
		}
	}
	return(result);
}

// Sequential read
uint8 _ReadSeq(uint16 fcbaddr) {
	CPM_FCB* F = (CPM_FCB*)_RamSysAddr(fcbaddr);
	uint8 result = 0xff;

	long fpos = ((F->s2 & MaxS2) * BlkS2 * BlkSZ) +
		(F->ex * BlkEX * BlkSZ) +
		(F->cr * BlkSZ);

	if (!_SelectDisk(F->dr)) {
		_FCBtoHostname(fcbaddr, &filename[0]);
		result = _sys_readseq(&filename[0], fpos);
		if (!result) {	// Read succeeded, adjust FCB
			++F->cr;
			if (F->cr > MaxCR) {
				F->cr = 1;
				++F->ex;
			}
			if (F->ex > MaxEX) {
				F->ex = 0;
				++F->s2;
			}
			if ((F->s2 & 0x7F) > MaxS2)
				result = 0xfe;	// (todo) not sure what to do 
		}
	}
	return(result);
}

// Sequential write
uint8 _WriteSeq(uint16 fcbaddr) {
	CPM_FCB* F = (CPM_FCB*)_RamSysAddr(fcbaddr);
	uint8 result = 0xff;

	long fpos = ((F->s2 & MaxS2) * BlkS2 * BlkSZ) +
		(F->ex * BlkEX * BlkSZ) +
		(F->cr * BlkSZ);

	if (!_SelectDisk(F->dr)) {
		if (!RW) {
			_FCBtoHostname(fcbaddr, &filename[0]);
			result = _sys_writeseq(&filename[0], fpos);
			if (!result) {	// Write succeeded, adjust FCB
				F->s2 &= 0x7F;		// reset unmodified flag
				++F->cr;
				if (F->cr > MaxCR) {
					F->cr = 1;
					++F->ex;
				}
				if (F->ex > MaxEX) {
					F->ex = 0;
					++F->s2;
				}
				if (F->s2 > MaxS2)
					result = 0xfe;	// (todo) not sure what to do 
			}
		} else {
			_error(errWRITEPROT);
		}
	}
	return(result);
}

// Random read
uint8 _ReadRand(uint16 fcbaddr) {
	CPM_FCB* F = (CPM_FCB*)_RamSysAddr(fcbaddr);
	uint8 result = 0xff;

	int32 record = (F->r2 << 16) | (F->r1 << 8) | F->r0;
	long fpos = record * BlkSZ;

	if (!_SelectDisk(F->dr)) {
		_FCBtoHostname(fcbaddr, &filename[0]);
		result = _sys_readrand(&filename[0], fpos);
		if (result == 0 || result == 1 || result == 4) {
			// adjust FCB unless error #6 (seek past 8MB - max CP/M file & disk size)
			F->cr = record & 0x7F;
			F->ex = (record >> 7) & 0x1f;
			if (F->s2 & 0x80) {
				F->s2 = ((record >> 12) & MaxS2) | 0x80;
			} else {
				F->s2 = (record >> 12) & MaxS2;
			}
		}
	}
	return(result);
}

// Random write
uint8 _WriteRand(uint16 fcbaddr) {
	CPM_FCB* F = (CPM_FCB*)_RamSysAddr(fcbaddr);
	uint8 result = 0xff;

	int32 record = (F->r2 << 16) | (F->r1 << 8) | F->r0;
	long fpos = record * BlkSZ;

	if (!_SelectDisk(F->dr)) {
		if (!RW) {
			_FCBtoHostname(fcbaddr, &filename[0]);
			result = _sys_writerand(&filename[0], fpos);
			if (!result) {	// Write succeeded, adjust FCB
				F->cr = record & 0x7F;
				F->ex = (record >> 7) & 0x1f;
				F->s2 = (record >> 12) & MaxS2;	// resets unmodified flag
			}
		} else {
			_error(errWRITEPROT);
		}
	}
	return(result);
}

// Returns the size of a CP/M file
uint8 _GetFileSize(uint16 fcbaddr) {
	CPM_FCB* F = (CPM_FCB*)_RamSysAddr(fcbaddr);
	uint8 result = 0xff;
	int32 count = _FileSize(DE) >> 7;

	if (count != -1) {
		F->r0 = count & 0xff;
		F->r1 = (count >> 8) & 0xff;
		F->r2 = (count >> 16) & 0xff;
	}
	return(result);
}

// Set the next random record
uint8 _SetRandom(uint16 fcbaddr) {
	CPM_FCB* F = (CPM_FCB*)_RamSysAddr(fcbaddr);
	uint8 result = 0x00;

	int32 count = F->cr & 0x7f;
	count += (F->ex & 0x1f) << 7;
	count += (F->s2 & MaxS2) << 12;

	F->r0 = count & 0xff;
	F->r1 = (count >> 8) & 0xff;
	F->r2 = (count >> 16) & 0xff;

	return(result);
}

// Sets the current user area
void _SetUser(uint8 user) {
	userCode = user & 0x1f;	// BDOS unoficially allows user areas 0-31
							// this may create folders from G-V if this function is called from an user program
							// It is an unwanted behavior, but kept as BDOS does it
#ifdef NOHIGHUSER
	if(userCode < 16)
#endif
	_MakeUserDir();			// Creates the user dir (0-F[G-V]) if needed
}

// Creates a disk directory folder
uint8 _MakeDisk(uint16 fcbaddr) {
	CPM_FCB* F = (CPM_FCB*)_RamSysAddr(fcbaddr);
	return(_sys_makedisk(F->dr));
}

// Checks if there's a temp submit file present
uint8 _CheckSUB(void) {
	uint8 result;
	uint8 oCode = userCode;							// Saves the current user code (original BDOS does not do this)
	_HostnameToFCB(tmpFCB, (uint8*)"$???????.???");	// The original BDOS in fact only looks for a file which start with $
#ifdef BATCHA
	_RamWrite(tmpFCB, 1);							// Forces it to be checked on drive A:
#endif
#ifdef BATCH0
	userCode = 0;									// Forces it to be checked on user 0
#endif
	result = (_SearchFirst(tmpFCB, FALSE) == 0x00) ? 0xff : 0x00;
	userCode = oCode;								// Restores the current user code
	return(result);
}

#ifdef HASLUA
// Executes a Lua script
uint8 _RunLua(uint16 fcbaddr) {
	uint8 luascript[17];
	uint8 result = 0xff;

	if (_FCBtoHostname(fcbaddr, &luascript[0])) {	// Script name must be unique
		if (!_SearchFirst(fcbaddr, FALSE)) {		// and must exist
			result = _RunLuaScript((char*)&luascript[0]);
		}
	}

	return(result);
}
#endif

#endif /* CPM_DISK_H */
