/*
 * CiderPress
 * Copyright (C) 2007 by faddenSoft, LLC.  All Rights Reserved.
 * See the file LICENSE for distribution terms.
 */
/*
 * Apple II CP/M disk format.
 *
 * Limitations:
 *	- Read-only.
 *	- Does not do much with user numbers.
 *	- Rumor has it that "sparse" files are possible.  Not handled.
 *	- I'm currently treating the directory as fixed-length.  This may
 *	  not be correct.
 *
 * As I have no practical experience with CP/M, this is the weakest of the
 * filesystem implementations.
 */
#include "StdAfx.h"
#include "DiskImgPriv.h"


/*
 * ===========================================================================
 *		DiskFSCPM
 * ===========================================================================
 */

const int kBlkSize = 512;				// really ought to be 1024
const int kVolDirBlock = 24;			// track 3 sector 0
const int kVolDirCount = 4;				// 4 prodos blocks
const int kNoDataByte = 0xe5;
const int kMaxUserNumber = 31;			// 0-15 on some systems, 0-31 on others
const int kMaxExtent = 31;				// extent counter, 0-31

/*
 * See if this looks like a CP/M volume.
 *
 * We test a few fields in the volume directory for validity.
 */
static DIError
TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder)
{
	DIError dierr = kDIErrNone;
	unsigned char dirBuf[kBlkSize * kVolDirCount];
	unsigned char* dptr;
	int i;

	assert(sizeof(dirBuf) == DiskFSCPM::kFullDirSize);

	for (i = 0; i < kVolDirCount; i++) {
		dierr = pImg->ReadBlockSwapped(kVolDirBlock + i, dirBuf + kBlkSize*i,
					imageOrder, DiskImg::kSectorOrderCPM);
		if (dierr != kDIErrNone)
			goto bail;
	}

	dptr = dirBuf;
	for (i = 0; i < DiskFSCPM::kFullDirSize/DiskFSCPM::kDirectoryEntryLen; i++)
	{
		if (*dptr != kNoDataByte) {
			/* usually userNumber == 0, but sometimes not; must be <= 31 */
			if (*dptr > kMaxUserNumber) {
				dierr = kDIErrFilesystemNotFound;
				break;
			}

			/* extent counter, 0-31 */
			if (dptr[12] > kMaxExtent) {
				dierr = kDIErrFilesystemNotFound;
				break;
			}
			/* value in S1 must be zero */
			if (dptr[13] != 0) {
				dierr = kDIErrFilesystemNotFound;
				break;
			}

			/* check for a valid filename here; high bit may be set on some bytes */
			unsigned char firstLet = *(dptr+1) & 0x7f;
			if (firstLet < 0x20) {
				dierr = kDIErrFilesystemNotFound;
				break;
			}
		}
		dptr += DiskFSCPM::kDirectoryEntryLen;
	}
	if (dierr == kDIErrNone) {
		WMSG1(" CPM found clean directory, imageOrder=%d\n", imageOrder);
	}

bail:
	return dierr;
}

/*
 * Test to see if the image is a CP/M disk.
 *
 * On the Apple II, these were always on 5.25" disks.  However, it's possible
 * to create hard drive volumes up to 8MB.
 */
/*static*/ DIError
DiskFSCPM::TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder,
    DiskImg::FSFormat* pFormat, FSLeniency leniency)
{
    /* CP/M disks use 1K blocks, so ignore anything with odd count */
    if (pImg->GetNumBlocks() == 0 ||
        (pImg->GetNumBlocks() & 0x01) != 0)
    {
        WMSG1(" CPM rejecting image with numBlocks=%ld\n",
            pImg->GetNumBlocks());
        return kDIErrFilesystemNotFound;
    }

	DiskImg::SectorOrder ordering[DiskImg::kSectorOrderMax];
	
	DiskImg::GetSectorOrderArray(ordering, *pOrder);

	for (int i = 0; i < DiskImg::kSectorOrderMax; i++) {
		if (ordering[i] == DiskImg::kSectorOrderUnknown)
			continue;
		if (TestImage(pImg, ordering[i]) == kDIErrNone) {
			*pOrder = ordering[i];
			*pFormat = DiskImg::kFormatCPM;
			return kDIErrNone;
		}
	}

	WMSG0(" CPM didn't find valid FS\n");
	return kDIErrFilesystemNotFound;
}

/*
 * Get things rolling.
 *
 * Since we're assured that this is a valid disk, errors encountered from here
 * on out must be handled somehow, possibly by claiming that the disk is
 * completely full and has no files on it.
 */
DIError
DiskFSCPM::Initialize(void)
{
	DIError dierr = kDIErrNone;

	dierr = ReadCatalog();
	if (dierr != kDIErrNone)
		goto bail;

	fVolumeUsage.Create(fpImg->GetNumBlocks());
	dierr = ScanFileUsage();
	if (dierr != kDIErrNone) {
		/* this might not be fatal; just means that *some* files are bad */
		dierr = kDIErrNone;
		goto bail;
	}

	fDiskIsGood = CheckDiskIsGood();

	fVolumeUsage.Dump();

	//A2File* pFile;
	//pFile = GetNextFile(nil);
	//while (pFile != nil) {
	//	pFile->Dump();
	//	pFile = GetNextFile(pFile);
	//}

bail:
	return dierr;
}

/*
 * Read the entire CP/M catalog (all 2K of it) into memory, and parse
 * out the individual files.
 *
 * A single file can have more than one directory entry.  We only want
 * to create an A2File object for the first one.
 */
DIError
DiskFSCPM::ReadCatalog(void)
{
	DIError dierr = kDIErrNone;
	unsigned char dirBuf[kFullDirSize];
	unsigned char* dptr;
	int i;

	for (i = 0; i < kVolDirCount; i++) {
		dierr = fpImg->ReadBlock(kVolDirBlock + i, dirBuf + kBlkSize*i);
		if (dierr != kDIErrNone)
			goto bail;
	}

	dptr = dirBuf;
	for (i = 0; i < kNumDirEntries; i++) {
		fDirEntry[i].userNumber = dptr[0x00];
		/* copy the filename, stripping the high bits off */
		for (int j = 0; j < kDirFileNameLen; j++)
			fDirEntry[i].fileName[j] = dptr[0x01 + j] & 0x7f;
		fDirEntry[i].fileName[kDirFileNameLen] = '\0';
		fDirEntry[i].extent = dptr[0x0c] + dptr[0x0e] * kExtentsInLowByte;
		fDirEntry[i].S1 = dptr[0x0d];
		fDirEntry[i].records = dptr[0x0f];
		memcpy(fDirEntry[i].blocks, &dptr[0x10], kDirEntryBlockCount);
		fDirEntry[i].readOnly = (dptr[0x09] & 0x80) != 0;
		fDirEntry[i].system = (dptr[0x0a] & 0x80) != 0;
		fDirEntry[i].badBlockList = false;	// set if block list is bad

		dptr += kDirectoryEntryLen;
	}

	/* create an entry for the first extent of each file */
	for (i = 0; i < kNumDirEntries; i++) {
		A2FileCPM* pFile;

		if (fDirEntry[i].userNumber == kNoDataByte || fDirEntry[i].extent != 0)
			continue;

		pFile = new A2FileCPM(this, fDirEntry);
		FormatName(pFile->fFileName, (char*)fDirEntry[i].fileName);
		pFile->fReadOnly = fDirEntry[i].readOnly;
		pFile->fDirIdx = i;

		pFile->fLength = 0;
		dierr = ComputeLength(pFile);
		if (dierr != kDIErrNone) {
			pFile->SetQuality(A2File::kQualityDamaged);
			dierr = kDIErrNone;
		}
		AddFileToList(pFile);
	}

	/*
	 * Validate the list of blocks.
	 */
	int maxCpmBlock;
	maxCpmBlock = (fpImg->GetNumBlocks() - kVolDirBlock) / 2;
	for (i = 0; i < kNumDirEntries; i++) {
		if (fDirEntry[i].userNumber == kNoDataByte)
			continue;
		for (int j = 0; j < kDirEntryBlockCount; j++) {
			if (fDirEntry[i].blocks[j] >= maxCpmBlock) {
				WMSG2(" CPM invalid block %d in file '%s'\n",
					fDirEntry[i].blocks[j], fDirEntry[i].fileName);
				//pFile->SetQuality(A2File::kQualityDamaged);
				fDirEntry[i].badBlockList = true;
				break;
			}
		}
	}

bail:
	return dierr;
}

/*
 * Reformat from 11 chars with spaces into clean xxxxx.yyy format.
 */
void
DiskFSCPM::FormatName(char* dstBuf, const char* srcBuf)
{
	char workBuf[kDirFileNameLen+1];
	char* cp;

	assert(strlen(srcBuf) < sizeof(workBuf));
	strcpy(workBuf, srcBuf);

	cp = workBuf;
	while (*cp != '\0') {
		//*cp &= 0x7f;		// [no longer necessary]
		if (*cp == ' ')
			*cp = '\0';
		if (*cp == ':')		// don't think this is allowed, but check
			*cp = 'X';		//  for it anyway
		cp++;
	}

	strcpy(dstBuf, workBuf);
	dstBuf[8] = '\0';		// in case filename part is full 8 chars
	strcat(dstBuf, ".");
	strcat(dstBuf, workBuf+8);

	assert(strlen(dstBuf) <= A2FileCPM::kMaxFileName);
}

/*
 * Compute the length of a file.  Sets "pFile->fLength".
 *
 * This requires walking through the list of extents and looking for the
 * last one.  We use the "records" field of the last extent to determine
 * the file length.
 *
 * (Should probably just get the block list and then walk that, rather than
 * having directory parse code in two places.)
 */
DIError
DiskFSCPM::ComputeLength(A2FileCPM* pFile)
{
	int i;
	int best, maxExtent;

	best = maxExtent = -1;

	for (i = 0; i < DiskFSCPM::kNumDirEntries; i++) {
		if (fDirEntry[i].userNumber == kNoDataByte)
			continue;

		if (strcmp((const char*)fDirEntry[i].fileName,
				   (const char*)fDirEntry[pFile->fDirIdx].fileName) == 0 &&
			fDirEntry[i].userNumber == fDirEntry[pFile->fDirIdx].userNumber)
		{
			/* this entry is part of the file */
			if (fDirEntry[i].extent > maxExtent) {
				best = i;
				maxExtent = fDirEntry[i].extent;
			}
		}
	}

	if (maxExtent < 0 || best < 0) {
		WMSG1("  CPM couldn't find existing file '%s'!\n", pFile->fFileName);
		assert(false);
		return kDIErrInternal;
	}

	pFile->fLength = kDirEntryBlockCount * 1024 * maxExtent +
						fDirEntry[best].records * 128;

	return kDIErrNone;
}


/*
 * Scan file usage into the volume usage map.
 *
 * Tracks 0, 1, and 2 are always used by the boot loader.  The volume directory
 * is on the first half of track 3 (blocks 0 and 1).
 */
DIError
DiskFSCPM::ScanFileUsage(void)
{
	int cpmBlock;
	int i, j;

	for (i = 0; i < kVolDirBlock; i++)
		SetBlockUsage(i, VolumeUsage::kChunkPurposeSystem);
	for (i = kVolDirBlock; i < kVolDirBlock + kVolDirCount; i++)
		SetBlockUsage(i, VolumeUsage::kChunkPurposeVolumeDir);

	for (i = 0; i < kNumDirEntries; i++) {
		if (fDirEntry[i].userNumber == kNoDataByte)
			continue;
		if (fDirEntry[i].badBlockList)
			continue;

		for (j = 0; j < kDirEntryBlockCount; j++) {
			cpmBlock = fDirEntry[i].blocks[j];
			if (cpmBlock == 0)
				break;
			SetBlockUsage(CPMToProDOSBlock(cpmBlock),
				VolumeUsage::kChunkPurposeUserData);
			SetBlockUsage(CPMToProDOSBlock(cpmBlock)+1,
				VolumeUsage::kChunkPurposeUserData);
		}
	}

	return kDIErrNone;
}

/*
 * Update an entry in the usage map.
 *
 * "block" is a 512-byte block, so you will have to call here twice for every
 * 1K CP/M block.
 */
void
DiskFSCPM::SetBlockUsage(long block, VolumeUsage::ChunkPurpose purpose)
{
	VolumeUsage::ChunkState cstate;

	if (fVolumeUsage.GetChunkState(block, &cstate) != kDIErrNone) {
		WMSG1(" CPM ERROR: unable to set state on block %ld\n", block);
		return;
	}

	if (cstate.isUsed) {
		cstate.purpose = VolumeUsage::kChunkPurposeConflict;
		WMSG1(" CPM conflicting uses for block=%ld\n", block);
	} else {
		cstate.isUsed = true;
		cstate.isMarkedUsed = true;		// no volume bitmap
		cstate.purpose = purpose;
	}
	fVolumeUsage.SetChunkState(block, &cstate);
}


/*
 * Scan for damaged files and conflicting file allocation entries.
 *
 * Appends some entries to the DiskImg notes, so this should only be run
 * once per DiskFS.
 *
 * Returns "true" if disk appears to be perfect, "false" otherwise.
 */
bool
DiskFSCPM::CheckDiskIsGood(void)
{
	//DIError dierr;
	bool result = true;

	//if (fEarlyDamage)
	//	result = false;

	/*
	 * TO DO: look for multiple files occupying the same blocks.
	 */

	/*
	 * Scan for "damaged" or "suspicious" files diagnosed earlier.
	 */
	bool damaged, suspicious;
	ScanForDamagedFiles(&damaged, &suspicious);

	if (damaged) {
		fpImg->AddNote(DiskImg::kNoteWarning,
			"One or more files are damaged.");
		result = false;
	} else if (suspicious) {
		fpImg->AddNote(DiskImg::kNoteWarning,
			"One or more files look suspicious.");
		result = false;
	}

	return result;
}


/*
 * ===========================================================================
 *		A2FileCPM
 * ===========================================================================
 */

/*
 * Not a whole lot to do, since there's no fancy index blocks.
 *
 * Calling GetBlockList twice is probably not the best way to go through life.
 * This needs an overhaul.
 */
DIError
A2FileCPM::Open(A2FileDescr** ppOpenFile, bool readOnly,
	bool rsrcFork /*=false*/)
{
	DIError dierr;
	A2FDCPM* pOpenFile = nil;

	if (fpOpenFile != nil)
		return kDIErrAlreadyOpen;
	if (rsrcFork)
		return kDIErrForkNotFound;

	assert(readOnly);

	pOpenFile = new A2FDCPM(this);

	dierr = GetBlockList(&pOpenFile->fBlockCount, nil);
	if (dierr != kDIErrNone)
		goto bail;

	pOpenFile->fBlockList = new unsigned char[pOpenFile->fBlockCount+1];
	pOpenFile->fBlockList[pOpenFile->fBlockCount] = 0xff;

	dierr = GetBlockList(&pOpenFile->fBlockCount, pOpenFile->fBlockList);
	if (dierr != kDIErrNone)
		goto bail;

	assert(pOpenFile->fBlockList[pOpenFile->fBlockCount] == 0xff);

	pOpenFile->fOffset = 0;
	//fOpen = true;

	fpOpenFile = pOpenFile;
	*ppOpenFile = pOpenFile;
	pOpenFile = nil;

bail:
	delete pOpenFile;
	return dierr;
}


/*
 * Get the complete block list for a file.  This will involve reading
 * one or more directory entries.
 *
 * Call this once with "blockBuf" equal to "nil" to get the block count,
 * then call a second time after allocating blockBuf.
 */
DIError
A2FileCPM::GetBlockList(long* pBlockCount, unsigned char* blockBuf) const
{
	di_off_t length = fLength;
	int blockCount = 0;
	int i, j;

	/*
	 * Run through the entries, pulling blocks out until we account for the
	 * entire length of the file.
	 *
	 * [Should probably pay more attention to extent numbers, making sure
	 * that they make sense.  Not vital until we allow writes.]
	 */
	for (i = 0; i < DiskFSCPM::kNumDirEntries; i++) {
		if (length <= 0)
			break;
		if (fpDirEntry[i].userNumber == kNoDataByte)
			continue;

		if (strcmp((const char*)fpDirEntry[i].fileName,
				   (const char*)fpDirEntry[fDirIdx].fileName) == 0 &&
			fpDirEntry[i].userNumber == fpDirEntry[fDirIdx].userNumber)
		{
			/* this entry is part of the file */
			for (j = 0; j < DiskFSCPM::kDirEntryBlockCount; j++) {
				if (fpDirEntry[i].blocks[j] == 0) {
					WMSG2(" CPM found sparse block %d/%d\n", i, j);
				}
				blockCount++;

				if (blockBuf != nil) {
					long listOffset = j +
						fpDirEntry[i].extent * DiskFSCPM::kDirEntryBlockCount;
					blockBuf[listOffset] = fpDirEntry[i].blocks[j];
				}

				length -= 1024;
				if (length <= 0)
					break;
			}
		}
	}

	if (length > 0) {
		WMSG1(" CPM WARNING: can't account for %ld bytes!\n", (long) length);
		//assert(false);
	}

	//WMSG2(" Returning blockCount=%d for '%s'\n", blockCount,
	//	fpDirEntry[fDirIdx].fileName);
	if (pBlockCount != nil) {
		assert(blockBuf == nil || *pBlockCount == blockCount);
		*pBlockCount = blockCount;
	}

	return kDIErrNone;
}

/*
 * Dump the contents of the A2File structure.
 */
void
A2FileCPM::Dump(void) const
{
	WMSG2("A2FileCPM '%s' length=%ld\n", fFileName, (long) fLength);
}


/*
 * ===========================================================================
 *		A2FDCPM
 * ===========================================================================
 */

/*
 * Read a chunk of data from the current offset.
 */
DIError
A2FDCPM::Read(void* buf, size_t len, size_t* pActual)
{
	WMSG3(" CP/M reading %d bytes from '%s' (offset=%ld)\n",
		len, fpFile->GetPathName(), (long) fOffset);

	A2FileCPM* pFile = (A2FileCPM*) fpFile;

	/* don't allow them to read past the end of the file */
	if (fOffset + (long)len > pFile->fLength) {
		if (pActual == nil)
			return kDIErrDataUnderrun;
		len = (size_t) (pFile->fLength - fOffset);
	}
	if (pActual != nil)
		*pActual = len;
	long incrLen = len;

	DIError dierr = kDIErrNone;
	const int kCPMBlockSize = kBlkSize*2;
	assert(kCPMBlockSize == 1024);
	unsigned char blkBuf[kCPMBlockSize];
	int blkIndex = (int) (fOffset / kCPMBlockSize);
	int bufOffset = (int) (fOffset % kCPMBlockSize);		// (& 0x3ff)
	size_t thisCount;
	long prodosBlock;

	if (len == 0)
		return kDIErrNone;
	assert(pFile->fLength != 0);

	while (len) {
		if (blkIndex >= fBlockCount) {
			/* ran out of data */
			return kDIErrDataUnderrun;
		}

		if (fBlockList[blkIndex] == 0) {
			/*
			 * Sparse block.
			 */
			memset(blkBuf, kNoDataByte, sizeof(blkBuf));
		} else {
			/*
			 * Read one CP/M block (two ProDOS blocks) and pull out the
			 * set of data that the user wants.
			 */
			prodosBlock = DiskFSCPM::CPMToProDOSBlock(fBlockList[blkIndex]);

			dierr = fpFile->GetDiskFS()->GetDiskImg()->ReadBlock(prodosBlock,
						blkBuf);
			if (dierr != kDIErrNone) {
				WMSG1(" CP/M error1 reading file '%s'\n", pFile->fFileName);
				return dierr;
			}
			dierr = fpFile->GetDiskFS()->GetDiskImg()->ReadBlock(prodosBlock+1,
						blkBuf + kBlkSize);
			if (dierr != kDIErrNone) {
				WMSG1(" CP/M error2 reading file '%s'\n", pFile->fFileName);
				return dierr;
			}
		}

		thisCount = kCPMBlockSize - bufOffset;
		if (thisCount > len)
			thisCount = len;

		memcpy(buf, blkBuf + bufOffset, thisCount);
		len -= thisCount;
		buf = (char*)buf + thisCount;

		bufOffset = 0;
		blkIndex++;
	}

	fOffset += incrLen;

	return dierr;
}

/*
 * Write data at the current offset.
 */
DIError
A2FDCPM::Write(const void* buf, size_t len, size_t* pActual)
{
	return kDIErrNotSupported;
}

/*
 * Seek to a new offset.
 */
DIError
A2FDCPM::Seek(di_off_t offset, DIWhence whence)
{
	di_off_t fileLength = ((A2FileCPM*) fpFile)->fLength;

	switch (whence) {
	case kSeekSet:
		if (offset < 0 || offset > fileLength)
			return kDIErrInvalidArg;
		fOffset = offset;
		break;
	case kSeekEnd:
		if (offset > 0 || offset < -fileLength)
			return kDIErrInvalidArg;
		fOffset = fileLength + offset;
		break;
	case kSeekCur:
		if (offset < -fOffset ||
			offset >= (fileLength - fOffset))
		{
			return kDIErrInvalidArg;
		}
		fOffset += offset;
		break;
	default:
		assert(false);
		return kDIErrInvalidArg;
	}

	assert(fOffset >= 0 && fOffset <= fileLength);
	return kDIErrNone;
}

/*
 * Return current offset.
 */
di_off_t
A2FDCPM::Tell(void)
{
	return fOffset;
}

/*
 * Release file state, such as it is.
 */
DIError
A2FDCPM::Close(void)
{
	fpFile->CloseDescr(this);
	return kDIErrNone;
}

/*
 * Return the #of sectors/blocks in the file.
 */
long
A2FDCPM::GetSectorCount(void) const
{
	return fBlockCount * 4;
}
long
A2FDCPM::GetBlockCount(void) const
{
	return fBlockCount * 2;
}

/*
 * Return the Nth track/sector in this file.
 */
DIError
A2FDCPM::GetStorage(long sectorIdx, long* pTrack, long* pSector) const
{
	long cpmIdx = sectorIdx / 4;	// 4 256-byte sectors per 1K CP/M block
	if (cpmIdx >= fBlockCount)
		return kDIErrInvalidIndex;	// CP/M files can have *no* storage

	long cpmBlock = fBlockList[cpmIdx];
	long prodosBlock = DiskFSCPM::CPMToProDOSBlock(cpmBlock);
	if (sectorIdx & 0x02)
		prodosBlock++;

	BlockToTrackSector(prodosBlock, (sectorIdx & 0x01) != 0, pTrack, pSector);
	return kDIErrNone;
}
/*
 * Return the Nth 512-byte block in this file.  Since things aren't stored
 * in 512-byte blocks, we grab the appropriate 1K block and pick half.
 */
DIError
A2FDCPM::GetStorage(long blockIdx, long* pBlock) const
{
	long cpmIdx = blockIdx / 2;	// 4 256-byte sectors per 1K CP/M block
	if (cpmIdx >= fBlockCount)
		return kDIErrInvalidIndex;

	long cpmBlock = fBlockList[cpmIdx];
	long prodosBlock = DiskFSCPM::CPMToProDOSBlock(cpmBlock);
	if (blockIdx & 0x01)
		prodosBlock++;

	*pBlock = prodosBlock;
	assert(*pBlock < fpFile->GetDiskFS()->GetDiskImg()->GetNumBlocks());
	return kDIErrNone;
}
