/*  PCSX2 - PS2 Emulator for PCs
 *  Copyright (C) 2002-2010  PCSX2 Dev Team
 *
 *  PCSX2 is free software: you can redistribute it and/or modify it under the terms
 *  of the GNU Lesser General Public License as published by the Free Software Found-
 *  ation, either version 3 of the License, or (at your option) any later version.
 *
 *  PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 *  without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
 *  PURPOSE.  See the GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License along with PCSX2.
 *  If not, see <http://www.gnu.org/licenses/>.
 */


#include "PrecompiledHeader.h"
#include "IopCommon.h"
#include "IsoFileFormats.h"

#include <errno.h>

static const uint BlockDumpHeaderSize = 16;

bool isoFile::detect()
{
	u8 buf[2456];
	u8* pbuf;

	ReadBlock(buf, 16);

	pbuf = buf + 24;

	if (strncmp((char*)(pbuf+1), "CD001", 5)) return false; // Not ISO 9660 compliant

	if (*(u16*)(pbuf+166) == 2048)
		m_type = ISOTYPE_CD;
	else
		m_type = ISOTYPE_DVD;

	return true; // We can deal with this.
}

void isoFile::_ReadDtable()
{
	_IsoPart& headpart( m_parts[0] );

	wxFileOffset flen = headpart.handle->GetLength();
	static const wxFileOffset datalen = flen - BlockDumpHeaderSize;
	pxAssert( (datalen % (m_blocksize + 4)) == 0);

	m_dtablesize	= datalen / (m_blocksize + 4);
	m_dtable		= new u32[m_dtablesize];

	headpart.Seek(BlockDumpHeaderSize);

	for (int i=0; i < m_dtablesize; ++i)
	{
		headpart.Read(m_dtable[i]);
		headpart.Seek(m_blocksize, wxFromCurrent);
	}
}

bool isoFile::tryIsoType(u32 _size, s32 _offset, s32 _blockofs)
{
	m_blocksize	= _size;
	m_offset	= _offset;
	m_blockofs	= _blockofs;

	return detect();
}

// based on florin's CDVDbin detection code :)
// Parameter:
//
// 
// Returns true if the image is valid/known/supported, or false if not (type == ISOTYPE_ILLEGAL).
bool isoFile::Detect( bool readType )
{
	char buf[32];

	m_type = ISOTYPE_ILLEGAL;

	_IsoPart& headpart( m_parts[0] );

	headpart.Seek( 0 );
	headpart.Read( buf, 4 );

	if (strncmp(buf, "BDV2", 4) == 0)
	{
		m_flags = ISOFLAGS_BLOCKDUMP_V2;
		headpart.Read(m_blocksize);
		headpart.Read(m_blocks);
		headpart.Read(m_blockofs);

		if (readType)
		{
			_ReadDtable();
			return detect();
		}
		return true;
	}

	// First sanity check: no sane CD image has less than 16 sectors, since that's what
	// we need simply to contain a TOC.  So if the file size is not large enough to
	// accommodate that, it is NOT a CD image --->

	wxFileOffset size = headpart.handle->GetLength();

	if (size < (2048 * 16)) return false;

	m_blocks = 16;

	if (tryIsoType(2048, 0, 24)) return true;				// ISO 2048
	if (tryIsoType(2336, 0, 16)) return true;				// RAW 2336
	if (tryIsoType(2352, 0, 0)) return true; 				// RAW 2352
	if (tryIsoType(2448, 0, 0)) return true; 				// RAWQ 2448

	if (tryIsoType(2048, 150 * 2048, 24)) return true;		// NERO ISO 2048
	if (tryIsoType(2352, 150 * 2048, 0)) return true;		// NERO RAW 2352
	if (tryIsoType(2448, 150 * 2048, 0)) return true;		// NERO RAWQ 2448

	if (tryIsoType(2048, -8, 24)) return true; 				// ISO 2048
	if (tryIsoType(2352, -8, 0)) return true;				// RAW 2352
	if (tryIsoType(2448, -8, 0)) return true;				// RAWQ 2448

	m_offset	= 0;
	m_blocksize	= CD_FRAMESIZE_RAW;
	m_blockofs	= 0;
	m_type		= ISOTYPE_AUDIO;

	//BUG: This also detects a memory-card-file as a valid Audio-CD ISO... -avih
	return true;
}

// Generates format header information for blockdumps.
void isoFile::WriteFormat(int _blockofs, uint _blocksize, uint _blocks)
{
	m_blocksize	= _blocksize;
	m_blocks	= _blocks;
	m_blockofs	= _blockofs;

	Console.WriteLn("blockoffset = %d", m_blockofs);
	Console.WriteLn("blocksize   = %u", m_blocksize);
	Console.WriteLn("blocks	     = %u", m_blocks);

	if (m_flags & ISOFLAGS_BLOCKDUMP_V2)
	{
		outWrite("BDV2", 4);
		outWrite(m_blocksize);
		outWrite(m_blocks);
		outWrite(m_blockofs);
	}
}

void isoFile::_ReadBlockD(u8* dst, uint lsn)
{
	_IsoPart& headpart( m_parts[0] );

//	Console.WriteLn("_isoReadBlockD %u, blocksize=%u, blockofs=%u\n", lsn, iso->blocksize, iso->blockofs);

	memset(dst, 0, m_blockofs);
	for (int i = 0; i < m_dtablesize; ++i)
	{
		if (m_dtable[i] != lsn) continue;

		// We store the LSN (u32) along with each block inside of blockdumps, so the
		// seek position ends up being based on (m_blocksize + 4) instead of just m_blocksize.

#ifdef PCSX2_DEBUG
		u32 check_lsn;
		headpart.Seek( BlockDumpHeaderSize + (i * (m_blocksize + 4)) );
		m_parts[0].Read( check_lsn );
		pxAssert( check_lsn == lsn );
#else
		headpart.Seek( BlockDumpHeaderSize + (i * (m_blocksize + 4)) + 4 );
#endif

		m_parts[0].Read( dst + m_blockofs, m_blocksize );
		return;
	}

	Console.WriteLn("Block %u not found in dump", lsn);
}

void isoFile::_ReadBlock(u8* dst, uint lsn)
{
	pxAssertMsg(lsn <= m_blocks,	"Invalid lsn passed into isoFile::_ReadBlock.");
	pxAssertMsg(m_numparts,			"Invalid isoFile object state; an iso file needs at least one part!");

	uint i;
	for (i = 0; i < m_numparts-1; ++i)
	{
		// lsn indexes should always go in order; use an assertion just to be sure:
		pxAssert(lsn >= m_parts[i].slsn);
		if (lsn <= m_parts[i].elsn) break;
	}

	wxFileOffset ofs = (wxFileOffset)(lsn - m_parts[i].slsn) * m_blocksize + m_offset;

//	Console.WriteLn("_isoReadBlock %u, blocksize=%u, blockofs=%u\n", lsn, iso->blocksize, iso->blockofs);

	memset(dst, 0, m_blockofs);
	m_parts[i].Seek(ofs);
	m_parts[i].Read(dst + m_blockofs, m_blocksize);
}

void isoFile::ReadBlock(u8* dst, uint lsn)
{
	if (lsn > m_blocks)
	{
		FastFormatUnicode msg;
		msg.Write("isoFile error: Block index is past the end of file! (%u > %u).", lsn, m_blocks);

		pxAssertDev(false, msg);
		Console.Error(msg);

		// [TODO] : Throw exception?
		//  Typically an error like this is bad; indicating an invalid dump or corrupted
		//  iso file.

		return;
	}

	if (m_flags == ISOFLAGS_BLOCKDUMP_V2)
		_ReadBlockD(dst, lsn);
	else
		_ReadBlock(dst, lsn);

	if (m_type == ISOTYPE_CD)
	{
		lsn_to_msf(dst + 12, lsn);
		dst[15] = 2;
	}
}

void isoFile::_WriteBlock(const u8* src, uint lsn)
{
	wxFileOffset ofs = (wxFileOffset)lsn * m_blocksize + m_offset;

	m_outstream->SeekO( ofs );
	outWrite( src + m_blockofs, m_blocksize );
}

void isoFile::_WriteBlockD(const u8* src, uint lsn)
{
	// Find and ignore blocks that have already been dumped:
	for (int i=0; i<m_dtablesize; ++i)
	{
		if (m_dtable[i] == lsn) return;
	}

	m_dtable[m_dtablesize++] = lsn;
	outWrite<u32>( lsn );
	outWrite( src + m_blockofs, m_blocksize );
}

void isoFile::WriteBlock(const u8* src, uint lsn)
{
	if (m_flags == ISOFLAGS_BLOCKDUMP_V2)
		_WriteBlockD(src, lsn);
	else
		_WriteBlock(src, lsn);
}

// --------------------------------------------------------------------------------------
//  IsoFile (implementations) : Init / Open / Create
// --------------------------------------------------------------------------------------

isoFile::isoFile()
{
	_init();
}

isoFile::~isoFile() throw()
{
	Close();
}

void isoFile::_init()
{
	m_type			= ISOTYPE_ILLEGAL;
	m_flags			= 0;

	m_offset		= 0;
	m_blockofs		= 0;
	m_blocksize		= 0;
	m_blocks		= 0;

	m_dtable		= 0;
	m_dtablesize	= 0;
}

// Tests for a filename extension in both upper and lower case, if the filesystem happens
// to be case-sensitive.
bool pxFileExists_WithExt( const wxFileName& filename, const wxString& ext )
{
	wxFileName part1 = filename;
	part1.SetExt( ext.Lower() );

	if (part1.FileExists()) return true;
	if (!wxFileName::IsCaseSensitive()) return false;

	part1.SetExt( ext.Upper() );
	return part1.FileExists();
}

void pxStream_OpenCheck( const wxStreamBase& stream, const wxString& fname, const wxString& mode )
{
	if (stream.IsOk()) return;

	ScopedExcept ex(Exception::FromErrno(fname, errno));
	ex->SetDiagMsg( pxsFmt(L"Unable to open the file for %s: %s", mode.c_str(), ex->DiagMsg().c_str()) );
	ex->Rethrow();
}

// multi-part ISO support is provided for FAT32 compatibility; so that large 4GB+ isos
// can be split into multiple smaller files.
//
// Returns TRUE if multiple parts for the ISO are found.  Returns FALSE if only one
// part is found.
void isoFile::FindParts()
{
	wxFileName nameparts( m_filename );
	wxString curext( nameparts.GetExt() );
	wxChar prefixch = wxTolower(curext[0]);

	// Multi-part rules!
	//  * The first part can either be the proper extension (ISO, MDF, etc) or the numerical
	//    extension (I00, I01, M00, M01, etc).
	//  * Numerical extensions MUST begin at 00 (I00 etc), regardless of if the first part
	//    is proper or numerical.

	uint i = 0;

	if ((curext.Length() == 3) && (curext[1] == L'0') && (curext[2] == L'0'))
	{
		// First file is an OO, so skip 0 in the loop below:
		i = 1;
	}

	FastFormatUnicode extbuf;

	extbuf.Write( L"%c%02u", prefixch, i );
	nameparts.SetExt( extbuf );
	if (!pxFileExists_WithExt(nameparts, extbuf)) return;

	DevCon.WriteLn( Color_Blue, "isoFile: multi-part %s detected...", curext.Upper().c_str() );
	ConsoleIndentScope indent;

	for (; i < MaxSplits; ++i)
	{
		extbuf.Clear();
		extbuf.Write( L"%c%02u", prefixch, i );
		if (!pxFileExists_WithExt(nameparts, extbuf)) break;

		_IsoPart& thispart( m_parts[m_numparts] );

		thispart.handle = new wxFileInputStream( nameparts.GetFullPath() );
		pxStream_OpenCheck( *thispart.handle, nameparts.GetFullPath(), L"reading" );

		m_blocks += thispart.CalculateBlocks( m_blocks, m_blocksize );

		DevCon.WriteLn( Color_Blue, L"\tblocks %u - %u in: %s",
			thispart.slsn, thispart.elsn,
			nameparts.GetFullPath().c_str()
		);
		++m_numparts;
	}

	//Console.WriteLn( Color_Blue, "isoFile: multi-part ISO loaded (%u parts found)", m_numparts );
}

// Tests the specified filename to see if it is a supported ISO type.  This function typically
// executes faster than isoFile::Open since it does not do the following:
//  * check for multi-part ISOs.  I tests for header info in the main/root ISO only.
//  * load blockdump indexes.
//
// Note that this is a member method, and that it will clobber any existing ISO state.
// (assertions are generated in debug mode if the object state is not already closed).
bool isoFile::Test( const wxString& srcfile )
{
	pxAssertMsg( !m_parts[0].handle, "Warning!  isoFile::Test is about to clobber whatever existing iso bound to this isoFile object!" );

	Close();
	m_filename = srcfile;

	m_parts[0].handle = new wxFileInputStream( m_filename );
	pxStream_OpenCheck( *m_parts[0].handle, m_filename, L"reading" );

	m_numparts		= 1;
	m_parts[0].slsn = 0;

	// elsn is unknown at this time, but is also unused when m_numparts == 1.
	// (and if numparts is incremented, elsn will get assigned accordingly)

	return Detect( false );
}

void isoFile::Open( const wxString& srcfile )
{
	Close();
	m_filename = srcfile;

	m_parts[0].handle = new wxFileInputStream( m_filename );
	pxStream_OpenCheck( *m_parts[0].handle, m_filename, L"reading" );

	m_numparts		= 1;
	m_parts[0].slsn = 0;
	
	// elsn is unknown at this time, but is also unused when m_numparts == 1.
	// (and if numparts is incremented, elsn will get assigned accordingly)
	
	if (!Detect())
		throw Exception::BadStream()
			.SetUserMsg(_("Unrecognized ISO image file format"))
			.SetDiagMsg(L"ISO mounting failed: PCSX2 is unable to identify the ISO image type.");

	if (!(m_flags & ISOFLAGS_BLOCKDUMP_V2))
	{
		m_blocks = m_parts[0].CalculateBlocks( 0, m_blocksize );
		FindParts();
		if (m_numparts > 1)
		{
			Console.WriteLn( Color_Blue, "isoFile: multi-part ISO detected.  %u parts found." );
		}
	}

	const char* isotypename = NULL;
    switch(m_type)
    {
        case ISOTYPE_CD:	isotypename = "CD";			break;
        case ISOTYPE_DVD:	isotypename = "DVD";		break;
        case ISOTYPE_AUDIO:	isotypename = "Audio CD";	break;

        case ISOTYPE_DVDDL:
			isotypename = "DVD9 (dual-layer)";
		break;

        case ISOTYPE_ILLEGAL:
        default:
			isotypename = "illegal media";
		break;
    }

	Console.WriteLn(Color_StrongBlue, L"isoFile open ok: %s", m_filename.c_str());

	ConsoleIndentScope indent;
	Console.WriteLn("Image type  = %s", isotypename); 
	Console.WriteLn("Fileparts   = %u", m_numparts);
	DevCon.WriteLn ("blocks      = %u", m_blocks);
	DevCon.WriteLn ("offset      = %d", m_offset);
	DevCon.WriteLn ("blocksize   = %u", m_blocksize);
	DevCon.WriteLn ("blockoffset = %d", m_blockofs);
}

void isoFile::Create(const wxString& filename, int flags)
{
	Close();
	m_filename = filename;

	m_flags		= flags;
	m_offset	= 0;
	m_blockofs	= 24;
	m_blocksize	= 2048;

	m_outstream = new wxFileOutputStream( m_filename );
	pxStream_OpenCheck( *m_outstream, m_filename, L"writing" );

	Console.WriteLn("isoFile create ok: %s ", m_filename.c_str());
}

void isoFile::Close()
{
	for (uint i=0; i<MaxSplits; ++i)
		m_parts[i].handle.Delete();

	m_dtable.Delete();

	_init();
}

bool isoFile::IsOpened() const
{
	return m_parts[0].handle && m_parts[0].handle->IsOk();
}

void isoFile::outWrite( const void* src, size_t size )
{
	m_outstream->Write(src, size);
	if(m_outstream->GetLastError() == wxSTREAM_WRITE_ERROR)
	{
		int err = errno;
		if (!err)
			throw Exception::BadStream(m_filename).SetDiagMsg(pxsFmt(L"An error occurred while writing %u bytes to file", size));

		ScopedExcept ex(Exception::FromErrno(m_filename, err));
		ex->SetDiagMsg( pxsFmt(L"An error occurred while writing %u bytes to file: %s", size, ex->DiagMsg().c_str()) );
		ex->Rethrow();
	}
}

// --------------------------------------------------------------------------------------
//  _IsoPart
// --------------------------------------------------------------------------------------

_IsoPart::~_IsoPart() throw()
{
}

void _IsoPart::Read( void* dest, size_t size )
{
	handle->Read(dest, size);
	if (handle->GetLastError() == wxSTREAM_READ_ERROR)
	{
		int err = errno;
		if (!err)
			throw Exception::BadStream(filename).SetDiagMsg(L"Cannot read from file (bad file handle?)");

		ScopedExcept ex(Exception::FromErrno(filename, err));
		ex->SetDiagMsg( L"cannot read from file: " + ex->DiagMsg() );
		ex->Rethrow();
	}

	// IMPORTANT!  The underlying file/source Eof() stuff is not really reliable, so we
	// must always use the explicit check against the number of bytes read to determine
	// end-of-stream conditions.

	// Throwing exceptions here ends emulation. 
	// We should let the game decide what to do with missing data though.
	if ((size_t)handle->LastRead() < size)
		Console.Warning( "ISO read problem (Bad game rip?)" );
		//throw Exception::EndOfStream( filename );
}

void _IsoPart::Seek(wxFileOffset pos, wxSeekMode mode)
{
	handle->SeekI(pos, mode);
}

void _IsoPart::SeekEnd(wxFileOffset pos)
{
	handle->SeekI(pos, wxFromEnd);
}

wxFileOffset _IsoPart::Tell() const
{
	return handle->TellI();
}

// returns the number of blocks contained in this part of the iso image.
uint _IsoPart::CalculateBlocks( uint startBlock, uint blocksize )
{
	wxFileOffset partsize = handle->GetLength();

	slsn = startBlock;
	uint numBlocks = partsize / blocksize;
	elsn = startBlock + numBlocks - 1;
	return numBlocks;
}
