////////////////////////////////////////////////////////////////////////////////
// ISO9660.cpp
// Class for reading ISO 9660 image files
// Copyright Bjoern Ganster 2010
////////////////////////////////////////////////////////////////////////////////

#include "BGCPString.h"
#include "ISO9660.h"

#include <algorithm>
#include <iostream>
#include <memory>

#define FILE_NUMBER FILE_NO_ISO_9660_CPP

ISO9660::ISO9660()
: MWArchive("ISO9660", ISO9660_ID)
{}

ISO9660::~ISO9660()
{}

// Open CD image file
bool ISO9660::open (const PathChar* PRJFN, bool /*readOnly*/)
{
	SET_SCOPE;
	bool openedSuccessfully = fc.openReadOnly(PRJFN);

	if (!openedSuccessfully) {
		return false;
	}

	// Analyze volume descriptor
	// A CD can have several volume descriptors, but we only need the first volume descriptor
	// Each volume descriptor seems to take up a CD sector
	SET_OP;
	char data[SectorSize];
	int offset = 16*SectorSize;
	bool readSuccess = fc.read(offset, data, SectorSize);

	if (!readSuccess 
	||  (data[17] != 'C')
	||  (data[18] != 'D')
	||  (data[19] != '0')
	||  (data[20] != '0')
	||  (data[21] != '1'))
	{
		COUT << "Not a CD image? CD001 marker not found where it is expected." << std::endl;
		return false;
	}
	
	// Try to read the root directory
	SET_OP;
	DWORD rootDirSector = *((DWORD*) (&data[174]));
	DWORD rootDirSize = *((DWORD*) (&data[182]));
	//printf ("Root directory sector: %i, %i bytes\n", rootDirSector, rootDirSize);
	readDirectory(rootDirSector, rootDirSize);
	m_parentDirStartSector.push_back(rootDirSector);
	m_parentDirSize.push_back(rootDirSize);

	// See if we can find a cue sheet file to indicate music tracks
	MemoryBlock cueMB;
	BGCPString cueFN = BGCPString(PRJFN, sizeof(PathChar) * (bgstrlen(PRJFN)-4), CP_SYSTEM) + ".cue";
	cueMB.loadFromFile(cueFN.getAsPath());

	size_t cueOffset = 0;
	size_t eol = 0;
	size_t sol = 0;

	// Parse lines in the cue sheet
	SET_OP;
	std::vector<BGCPString> trackNames;
	std::vector<FileOffset> trackStarts;
	BGCPString trackName, trackType;
	while (cueOffset < cueMB.getSize()) {
		cueMB.findEOL<char>(cueOffset, eol, sol);
		BGCPString line(cueMB.getPtr<char>(cueOffset), eol-cueOffset, CP_UTF8);
		cueOffset = sol;

		if (line.startsWith("   TRACK")) {
			trackName = line.copyPart(3, line.getByteCount()-9);
			trackType = line.copyPart(trackName.getByteCount()+4, line.getByteCount()-trackName.getByteCount()-4);
		} else if (trackType == "AUDIO" && line.startsWith("   INDEX 1")) {
			BGCPString minutesStr = line.copyPart(11, 2);
			BGCPString secondsStr = line.copyPart(14, 2);
			BGCPString framesStr  = line.copyPart(17, 2);
			FileOffset trackStart = (FileOffset)(SectorSize * framesStr.toInt() + 
				75 * SectorSize * secondsStr.toInt() + 60 * 75 * SectorSize * minutesStr.toInt());
			trackNames.emplace_back(trackName);
			trackStarts.emplace_back(trackStart);
		}
	}

	// Add one entry to the trackStarts to simplify things below ...
	trackStarts.emplace_back(fc.getFileSize());

	// Add track files to the root directory ...
	for (size_t i = 0; i < trackNames.size(); ++i) {
		addFile(trackNames[i] + ".cda", trackStarts[i+1]-trackStarts[i], trackStarts[i], ISO_9660_AUDIO);
	}

	RETURN true;
}

// Close file
void ISO9660::close()
{
	fc.close();
	forgetDirectoryContents();
}

void ISO9660::forgetDirectoryContents()
{
	mEntries.clear();
	m_isDirectory.clear();
}

// Read extent (directory or file data)
bool ISO9660::readExtent(int sector, void* buf, int toRead, int startOffset)
{
	SET_SCOPE;
	if (startOffset > SectorSize) {
		sector += (startOffset + SectorSize-1) / SectorSize;
		startOffset = startOffset % SectorSize;
	}

	char* bufC = (char*) buf;
	while (toRead > 0) {
		int readNow = std::min(toRead, 2048);
		int offset = sector*SectorSize + 16 + startOffset;
		if (fc.read(offset, bufC, readNow)) {
			toRead -= readNow;
			bufC += readNow;
			++sector;
			startOffset = 0;
		} else {
			RETURN false;
		}
	}

	RETURN true;
}

// Read bufSize bytes from file numbered index at offset into buf
bool ISO9660::getFileChunk(size_t index, size_t offset, void* buf, size_t bufSize)
{
	SET_SCOPE;
	int toRead = std::min(bufSize, mEntries[index].fileSize);

	if (mEntries[index].fileType == ISO_9660_AUDIO) {
		// We computed the offsets for reading audio already, and audio data uses the full 2352 bytes in a CD-ROM sector.
		// Audio tracks can start at any frame in a sector
		if (offset > mEntries[index].fileSize) {
			RETURN false;
		}
		if (offset + bufSize > mEntries[index].fileSize) {
			bufSize = mEntries[index].fileSize - offset;
		}
		RETURN fc.read(mEntries[index].fileOffset + offset, (char*)buf, bufSize);
	} else {
		// CD-ROM data does not use a full CD-ROM sector, but can start only at a full sector
		RETURN readExtent(mEntries[index].fileOffset, buf, toRead, offset);
	}
}

// Enter subdirectory
bool ISO9660::enterSubDir (size_t index)
{
	SET_SCOPE;
	if (index < mEntries.size()) {
		int dirStartSector = mEntries[index].fileOffset;
		int dirSize = mEntries[index].fileSize;
		m_parentDirStartSector.push_back(dirStartSector);
		m_parentDirSize.push_back(dirSize);
		forgetDirectoryContents();
		RETURN readDirectory(dirStartSector, dirSize);
	} else
		RETURN false;
}

// Leave subdirectory
bool ISO9660::leaveDir()
{
	SET_SCOPE;
	if (inSubDir()) {
		m_parentDirStartSector.pop_back();
		m_parentDirSize.pop_back();
		int last = m_parentDirStartSector.size()-1;
		int dirStartSector = m_parentDirStartSector.back();
		int dirSize = m_parentDirSize.back();
		forgetDirectoryContents();
		RETURN readDirectory(dirStartSector, dirSize);
	} else
		RETURN false;
}

// Check if currently in sub directory
bool ISO9660::inSubDir() const
{
	return (m_parentDirStartSector.size()>1);
}

// Check if entry index is a directory
bool ISO9660::isDirectory (size_t index) const
{
	if (index < m_isDirectory.size()) {
		return m_isDirectory[index];
	} else {
		return 0;
	}
}

///////////////////////////////////////////////////////////////////////////////
// Read directory entries

#pragma pack (1)
struct ISO_9660_Directory_Entry {
	uint8_t entryLen;
	uint8_t ExtendedAttributeRecordLength;
	uint32_t startSector_LE;
	uint32_t startSector_BE;
	uint32_t fileSize_LE;
	uint32_t fileSize_BE;
	char recordDateTime[7];
	uint8_t flags;
	uint8_t unitSize;
	uint8_t gapSize;
	uint16_t VolumeSeqNo_LE;
	uint16_t VolumeSeqNo_BE;
	uint8_t FileNameLength;
};

bool ISO9660::readDirectory(int sector, int dirSize)
{
	SET_SCOPE;
	std::unique_ptr<char> data (new char[dirSize]);
	int offset = sector*SectorSize;
	//bool readSuccess = fc.read(offset, data, SectorSize);
	bool readSuccess = readExtent(sector, data.get(), dirSize, 0);
	int found = 0;

	if (readSuccess) {
		offset = 0;
		while (offset < dirSize) {
			ISO_9660_Directory_Entry* entry = (ISO_9660_Directory_Entry*)&data.get()[offset];
			if (entry->entryLen == 0) {
				//offset = dirSize; // Null-entry, done/abort
				offset = ((offset / 2048) + 1) * 2048;
			} else {
				int nameOffset = offset+sizeof(ISO_9660_Directory_Entry);
				BGCPString fn(data.get() + nameOffset, entry->FileNameLength, CP_ASCII);

				int stopPos = fn.findChar(';');
				if (stopPos >= 0) {
					fn.setByteCount(stopPos);
				}

				++found;
				if (found > 2) {
					addFile (fn, entry->fileSize_LE, entry->startSector_LE);
					if ((entry->flags & 2) > 0)
						m_isDirectory.push_back(true);
					else
						m_isDirectory.push_back(false);
				}
				offset += entry->entryLen;
			}
		}
	}

	RETURN readSuccess;
}
