/*
 * CiderPress
 * Copyright (C) 2007 by faddenSoft, LLC.  All Rights Reserved.
 * See the file LICENSE for distribution terms.
 */
/*
 * The "MicroDrive" DiskFS is a container class for multiple ProDOS and HFS
 * volumes.  It represents a partitioned disk device, such as a hard
 * drive or CF card, that has been formatted for use with ///SHH Systeme's
 * MicroDrive card for the Apple II.
 */
#include "StdAfx.h"
#include "DiskImgPriv.h"


const int kBlkSize = 512;
const int kPartMapBlock = 0;	// partition map lives here
const unsigned int kPartSizeMask = 0x00ffffff;


/*
 * Format of partition map.  It resides in the first 256 bytes of block 0.
 * All values are in little-endian order.
 *
 * The layout was discovered through reverse-engineering.  Additional notes:
 *
	From Joachim Lange:

	Below, this is the configuration block as it is used in all
	MicroDrive cards. Please verify that my ID shortcut can be
	found at offset 0, otherwise the partition info is not
	valid. Most of the other parms are not useful, some are
	historic and not useful anymore. As a second security
	measure, verify that the first partition starts at
	absolute block 256. This is also a fixed value used in all
	MicroDrive cards. Of course the partition size is not two
	bytes long but three (not four), the 4th byte is used for
	switching drives in a two-drive configuration. So, for
	completeness, when reading partition sizes, perform a
	partitionLength[..] & 0x00FFFFFF, or at least issue a
	warning that something may be wrong. The offset
	(partitionStart) could reach into the 4th byte.
	I have attached the config block in a zip file because
	the mailer would probably re-format the source text.
 */
const int kMaxNumParts = 8;
typedef struct DiskFSMicroDrive::PartitionMap {
	unsigned short	magic;				// partition signature
	unsigned short	cylinders;			// #of cylinders
	unsigned short	reserved1;			// ??
	unsigned short	heads;				// #of heads/cylinder
	unsigned short	sectors;			// #of sectors/track
	unsigned short	reserved2;			// ??
	unsigned char	numPart1;			// #of partitions in first chunk
	unsigned char	numPart2;			// #of partitions in second chunk
	unsigned char	reserved3[10];		// bytes 0x0e-0x17
	unsigned short	romVersion;			// IIgs ROM01 or ROM03
	unsigned char	reserved4[6];		// bytes 0x1a-0x1f
	unsigned long	partitionStart1[kMaxNumParts];	// bytes 0x20-0x3f
	unsigned long	partitionLength1[kMaxNumParts];	// bytes 0x40-0x5f
	unsigned char	reserved5[32];		// bytes 0x60-0x7f
	unsigned long	partitionStart2[kMaxNumParts];	// bytes 0x80-0x9f
	unsigned long	partitionLength2[kMaxNumParts];	// bytes 0xa0-0xbf

	unsigned char	padding[320];
} PartitionMap;


/*
 * Figure out if this is a MicroDrive partition.
 *
 * The "imageOrder" parameter has no use here, because (in the current
 * version) embedded parent volumes are implicitly ProDOS-ordered.
 */
/*static*/ DIError
DiskFSMicroDrive::TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder)
{
	DIError dierr = kDIErrNone;
	unsigned char blkBuf[kBlkSize];
	int partCount1, partCount2;

	assert(sizeof(PartitionMap) == kBlkSize);

	/*
	 * See if block 0 is a MicroDrive partition map.
	 */
	dierr = pImg->ReadBlockSwapped(kPartMapBlock, blkBuf, imageOrder,
				DiskImg::kSectorOrderProDOS);
	if (dierr != kDIErrNone)
		goto bail;

	if (GetShortLE(&blkBuf[0x00]) != kPartitionSignature) {
		WMSG0(" MicroDrive partition signature not found in first part block\n");
		dierr = kDIErrFilesystemNotFound;
		goto bail;
	}
	/* might assert that partCount2 be zero unless partCount1 == 8? */
	partCount1 = blkBuf[0x0c];
	partCount2 = blkBuf[0x0d];
	if (partCount1 == 0 || partCount1 > kMaxNumParts ||
		partCount2 > kMaxNumParts)
	{
		WMSG2(" MicroDrive unreasonable partCount values %d/%d\n",
			partCount1, partCount2);
		dierr = kDIErrFilesystemNotFound;
		goto bail;
	}

	/* consider testing other fields */

	// success!
	WMSG2(" MicroDrive partition map count = %d/%d\n", partCount1, partCount2);

bail:
	return dierr;
}


/*
 * Unpack a partition map block into a partition map data structure.
 */
/*static*/ void
DiskFSMicroDrive::UnpackPartitionMap(const unsigned char* buf,
	PartitionMap* pMap)
{
	pMap->magic = GetShortLE(&buf[0x00]);
	pMap->cylinders = GetShortLE(&buf[0x02]);
	pMap->reserved1 = GetShortLE(&buf[0x04]);
	pMap->heads = GetShortLE(&buf[0x06]);
	pMap->sectors = GetShortLE(&buf[0x08]);
	pMap->reserved2 = GetShortLE(&buf[0x0a]);
	pMap->numPart1 = buf[0x0c];
	pMap->numPart2 = buf[0x0d];
	memcpy(pMap->reserved3, &buf[0x0e], sizeof(pMap->reserved3));
	pMap->romVersion = GetShortLE(&buf[0x18]);
	memcpy(pMap->reserved4, &buf[0x1a], sizeof(pMap->reserved4));

	for (int i = 0; i < kMaxNumParts; i++) {
		pMap->partitionStart1[i] = GetLongLE(&buf[0x20] + i * 4);
		pMap->partitionLength1[i] = GetLongLE(&buf[0x40] + i * 4) & kPartSizeMask;
		pMap->partitionStart2[i] = GetLongLE(&buf[0x80] + i * 4);
		pMap->partitionLength2[i] = GetLongLE(&buf[0xa0] + i * 4) & kPartSizeMask;
	}
	memcpy(pMap->reserved5, &buf[0x60], sizeof(pMap->reserved5));
	memcpy(pMap->padding, &buf[0x80], sizeof(pMap->padding));
}

/*
 * Debug: dump the contents of the partition map.
 */
/*static*/ void
DiskFSMicroDrive::DumpPartitionMap(const PartitionMap* pMap)
{
	WMSG0(" MicroDrive partition map:\n");
	WMSG4("    cyls=%d res1=%d heads=%d sects=%d\n",
		pMap->cylinders, pMap->reserved1, pMap->heads, pMap->sectors);
	WMSG3("    res2=%d numPart1=%d numPart2=%d\n",
		pMap->reserved2, pMap->numPart1, pMap->numPart2);
	WMSG1("    romVersion=ROM%02d\n", pMap->romVersion);

	int i, parts;

	parts = pMap->numPart1;
	assert(parts <= kMaxNumParts);
	for (i = 0; i < parts; i++) {
		WMSG3("    %2d: startLBA=%8ld length=%ld\n",
			i, pMap->partitionStart1[i], pMap->partitionLength1[i]);
	}
	parts = pMap->numPart2;
	assert(parts <= kMaxNumParts);
	for (i = 0; i < parts; i++) {
		WMSG3("    %2d: startLBA=%8ld length=%ld\n",
			i+8, pMap->partitionStart2[i], pMap->partitionLength2[i]);
	}
}


/*
 * Open up a sub-volume.
 */
DIError
DiskFSMicroDrive::OpenSubVolume(long startBlock, long numBlocks)
{
	DIError dierr = kDIErrNone;
	DiskFS* pNewFS = nil;
	DiskImg* pNewImg = nil;
	//bool tweaked = false;

	WMSG2("Adding %ld +%ld\n", startBlock, numBlocks);

	if (startBlock > fpImg->GetNumBlocks()) {
		WMSG2("MicroDrive start block out of range (%ld vs %ld)\n",
			startBlock, fpImg->GetNumBlocks());
		return kDIErrBadPartition;
	}
	if (startBlock + numBlocks > fpImg->GetNumBlocks()) {
		WMSG2("MicroDrive partition too large (%ld vs %ld avail)\n",
			numBlocks, fpImg->GetNumBlocks() - startBlock);
		fpImg->AddNote(DiskImg::kNoteInfo,
			"Reduced partition from %ld blocks to %ld.\n",
			numBlocks, fpImg->GetNumBlocks() - startBlock);
		numBlocks = fpImg->GetNumBlocks() - startBlock;
		//tweaked = true;
	}

	pNewImg = new DiskImg;
	if (pNewImg == nil) {
		dierr = kDIErrMalloc;
		goto bail;
	}

	dierr = pNewImg->OpenImage(fpImg, startBlock, numBlocks);
	if (dierr != kDIErrNone) {
		WMSG3(" MicroDriveSub: OpenImage(%ld,%ld) failed (err=%d)\n",
			startBlock, numBlocks, dierr);
		goto bail;
	}

	//WMSG2("  +++ CFFASub: new image has ro=%d (parent=%d)\n",
	//	pNewImg->GetReadOnly(), pImg->GetReadOnly());

	/* figure out what the format is */
	dierr = pNewImg->AnalyzeImage();
	if (dierr != kDIErrNone) {
		WMSG1(" MicroDriveSub: analysis failed (err=%d)\n", dierr);
		goto bail;
	}

	/* we allow unrecognized partitions */
	if (pNewImg->GetFSFormat() == DiskImg::kFormatUnknown ||
		pNewImg->GetSectorOrder() == DiskImg::kSectorOrderUnknown)
	{
		WMSG2(" MicroDriveSub (%ld,%ld): unable to identify filesystem\n",
			startBlock, numBlocks);
		DiskFSUnknown* pUnknownFS = new DiskFSUnknown;
		if (pUnknownFS == nil) {
			dierr = kDIErrInternal;
			goto bail;
		}
		//pUnknownFS->SetVolumeInfo((const char*)pMap->pmParType);
		pNewFS = pUnknownFS;
		//pNewImg->AddNote(DiskImg::kNoteInfo, "Partition name='%s' type='%s'.",
		//	pMap->pmPartName, pMap->pmParType);
	} else {
		/* open a DiskFS for the sub-image */
		WMSG2(" MicroDriveSub (%ld,%ld) analyze succeeded!\n", startBlock, numBlocks);
		pNewFS = pNewImg->OpenAppropriateDiskFS(true);
		if (pNewFS == nil) {
			WMSG0(" MicroDriveSub: OpenAppropriateDiskFS failed\n");
			dierr = kDIErrUnsupportedFSFmt;
			goto bail;
		}
	}

	/* we encapsulate arbitrary stuff, so encourage child to scan */
	pNewFS->SetScanForSubVolumes(kScanSubEnabled);

	/*
	 * Load the files from the sub-image.  When doing our initial tests,
	 * or when loading data for the volume copier, we don't want to dig
	 * into our sub-volumes, just figure out what they are and where.
	 *
	 * If "initialize" fails, the sub-volume won't get added to the list.
	 * It's important that a failure at this stage doesn't cause the whole
	 * thing to fall over.
	 */
	InitMode initMode;
	if (GetScanForSubVolumes() == kScanSubContainerOnly)
		initMode = kInitHeaderOnly;
	else
		initMode = kInitFull;
	dierr = pNewFS->Initialize(pNewImg, initMode);
	if (dierr != kDIErrNone) {
		WMSG1(" MicroDriveSub: error %d reading list of files from disk", dierr);
		goto bail;
	}

	/* add it to the list */
	AddSubVolumeToList(pNewImg, pNewFS);
	pNewImg = nil;
	pNewFS = nil;

bail:
	delete pNewFS;
	delete pNewImg;
	return dierr;
}

/*
 * Check to see if this is a MicroDrive volume.
 */
/*static*/ DIError
DiskFSMicroDrive::TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder,
	DiskImg::FSFormat* pFormat, FSLeniency leniency)
{
	if (pImg->GetNumBlocks() < kMinInterestingBlocks)
		return kDIErrFilesystemNotFound;
	if (pImg->GetIsEmbedded())		// don't look for partitions inside
		return kDIErrFilesystemNotFound;

	/* assume ProDOS -- shouldn't matter, since it's embedded */
	if (TestImage(pImg, DiskImg::kSectorOrderProDOS) == kDIErrNone) {
		*pFormat = DiskImg::kFormatMicroDrive;
		*pOrder = DiskImg::kSectorOrderProDOS;
		return kDIErrNone;
	}

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


/*
 * Prep the MicroDrive "container" for use.
 */
DIError
DiskFSMicroDrive::Initialize(void)
{
	DIError dierr = kDIErrNone;

	WMSG1("MicroDrive initializing (scanForSub=%d)\n", fScanForSubVolumes);

	/* seems pointless *not* to, but we just do what we're told */
	if (fScanForSubVolumes != kScanSubDisabled) {
		dierr = FindSubVolumes();
		if (dierr != kDIErrNone)
			return dierr;
	}

	/* blank out the volume usage map */
	SetVolumeUsageMap();

	return dierr;
}


/*
 * Find the various sub-volumes and open them.
 */
DIError
DiskFSMicroDrive::FindSubVolumes(void)
{
	DIError dierr = kDIErrNone;
	unsigned char buf[kBlkSize];
	PartitionMap map;
	int i;

	dierr = fpImg->ReadBlock(kPartMapBlock, buf);
	if (dierr != kDIErrNone)
		goto bail;
	UnpackPartitionMap(buf, &map);
	DumpPartitionMap(&map);

	/* first part of the table */
	for (i = 0; i < map.numPart1; i++) {
		dierr = OpenVol(i,
							map.partitionStart1[i], map.partitionLength1[i]);
		if (dierr != kDIErrNone)
			goto bail;
	}

	/* second part of the table */
	for (i = 0; i < map.numPart2; i++) {
		dierr = OpenVol(i + kMaxNumParts,
							map.partitionStart2[i], map.partitionLength2[i]);
		if (dierr != kDIErrNone)
			goto bail;
	}

bail:
	return dierr;
}

/*
 * Open the volume.  If it fails, open a placeholder instead.  (If *that*
 * fails, return with an error.)
 */
DIError
DiskFSMicroDrive::OpenVol(int idx, long startBlock, long numBlocks)
{
	DIError dierr;

	dierr = OpenSubVolume(startBlock, numBlocks);
	if (dierr != kDIErrNone) {
		if (dierr == kDIErrCancelled)
			goto bail;
		DiskFS* pNewFS = nil;
		DiskImg* pNewImg = nil;

		WMSG1(" MicroDrive failed opening sub-volume %d\n", idx);
		dierr = CreatePlaceholder(startBlock, numBlocks, NULL, NULL,
					&pNewImg, &pNewFS);
		if (dierr == kDIErrNone) {
			AddSubVolumeToList(pNewImg, pNewFS);
		} else {
			WMSG1("  MicroDrive unable to create placeholder (err=%d)\n",
				dierr);
			// fall out with error
		}
	}

bail:
	return dierr;
}
