/*  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"

#define ENABLE_TIMESTAMPS

#ifdef _WIN32
#	include <wx/msw/wrapwin.h>
#endif

#include <ctype.h>
#include <time.h>
#include <wx/datetime.h>
#include <exception>
#include <memory>

#include "IsoFS/IsoFS.h"
#include "IsoFS/IsoFSCDVD.h"
#include "CDVDisoReader.h"

#include "DebugTools/SymbolMap.h"
#include "AppConfig.h"

const wxChar* CDVD_SourceLabels[] =
{
	L"ISO",
	L"Plugin",
	L"NoDisc",
	NULL
};

// ----------------------------------------------------------------------------
// diskTypeCached
// Internal disc type cache, to reduce the overhead of disc type checks, which are
// performed quite liberally by many games (perhaps intended to keep the PS2 DVD
// from spinning down due to idle activity?).
// Cache is set to -1 for init and when the disc is removed/changed, which invokes
// a new DiskTypeCheck.  All subsequent checks use the non-negative value here.
//
static int diskTypeCached = -1;

// used to bridge the gap between the old getBuffer api and the new getBuffer2 api.
int lastReadSize;
u32 lastLSN;		// needed for block dumping

// Records last read block length for block dumping
//static int plsn = 0;

static OutputIsoFile blockDumpFile;

// Assertion check for CDVD != NULL (in devel and debug builds), because its handier than
// relying on DEP exceptions -- and a little more reliable too.
static void CheckNullCDVD()
{
	pxAssertDev( CDVD != NULL, "Invalid CDVD object state (null pointer exception)" );
}

//////////////////////////////////////////////////////////////////////////////////////////
// Disk Type detection stuff (from cdvdGigaherz)
//
static int CheckDiskTypeFS(int baseType)
{
	IsoFSCDVD isofs;
	IsoDirectory rootdir(isofs);
	try {
		IsoFile file( rootdir, L"SYSTEM.CNF;1");

		int size = file.getLength();

		std::unique_ptr<char[]> buffer(new char[file.getLength() + 1]);
		file.read(buffer.get(),size);
		buffer[size]='\0';

		char* pos = strstr(buffer.get(), "BOOT2");
		if (pos == NULL)
		{
			pos = strstr(buffer.get(), "BOOT");
			if (pos == NULL)  return CDVD_TYPE_ILLEGAL;
			return CDVD_TYPE_PSCD;
		}

		return (baseType==CDVD_TYPE_DETCTCD) ? CDVD_TYPE_PS2CD : CDVD_TYPE_PS2DVD;
	}
	catch( Exception::FileNotFound& )
	{
	}

	try {
		IsoFile file( rootdir, L"PSX.EXE;1");
		return CDVD_TYPE_PSCD;
	}
	catch( Exception::FileNotFound& )
	{
	}

	try {
		IsoFile file( rootdir, L"VIDEO_TS/VIDEO_TS.IFO;1");
		return CDVD_TYPE_DVDV;
	}
	catch( Exception::FileNotFound& )
	{
	}

#ifdef PCSX2_DEVBUILD
	return CDVD_TYPE_PS2DVD; // need this hack for some homebrew (SMS)
#endif
	return CDVD_TYPE_ILLEGAL; // << Only for discs which aren't ps2 at all.
}

static int FindDiskType(int mType)
{
	int dataTracks = 0;
	int audioTracks = 0;
	int iCDType = mType;
	cdvdTN tn;

	CDVD->getTN(&tn);

	if (tn.strack != tn.etrack) // multitrack == CD.
	{
		iCDType = CDVD_TYPE_DETCTCD;
	}
	else if (mType < 0)
	{
		static u8 bleh[CD_FRAMESIZE_RAW];
		cdvdTD td;

		CDVD->getTD(0,&td);
		if (td.lsn > 452849)
		{
			iCDType = CDVD_TYPE_DETCTDVDS;
		}
		else
		{
			if (DoCDVDreadSector(bleh, 16, CDVD_MODE_2048) == 0)
			{
				//const cdVolDesc& volDesc = (cdVolDesc&)bleh;
				//if(volDesc.rootToc.tocSize == 2048)
				
				//Horrible hack! in CD images position 166 and 171 have block size but not DVD's
				//It's not always 2048 however (can be 4096)
				//Test Impossible Mission if thia is changed.
				if(*(u16*)(bleh+166) == *(u16*)(bleh+171))
					iCDType = CDVD_TYPE_DETCTCD;
				else
					iCDType = CDVD_TYPE_DETCTDVDS;
			}
		}
	}

	if (iCDType == CDVD_TYPE_DETCTDVDS)
	{
		s32 dlt = 0;
		u32 l1s = 0;

		if(CDVD->getDualInfo(&dlt,&l1s)==0)
		{
			if (dlt > 0) iCDType = CDVD_TYPE_DETCTDVDD;
		}
	}

	switch(iCDType)
	{
		case CDVD_TYPE_DETCTCD:
			Console.WriteLn(" * CDVD Disk Open: CD, %d tracks (%d to %d):", tn.etrack-tn.strack+1,tn.strack,tn.etrack);
			break;

		case CDVD_TYPE_DETCTDVDS:
			Console.WriteLn(" * CDVD Disk Open: DVD, Single layer or unknown:");
			break;

		case CDVD_TYPE_DETCTDVDD:
			Console.WriteLn(" * CDVD Disk Open: DVD, Double layer:");
			break;
	}

	audioTracks = dataTracks = 0;
	for(int i = tn.strack; i <= tn.etrack; i++)
	{
		cdvdTD td,td2;

		CDVD->getTD(i,&td);

		if (tn.etrack > i)
			CDVD->getTD(i+1,&td2);
		else
			CDVD->getTD(0,&td2);

		int tlength = td2.lsn - td.lsn;

		if (td.type == CDVD_AUDIO_TRACK)
		{
			audioTracks++;
			Console.WriteLn(" * * Track %d: Audio (%d sectors)", i,tlength);
		}
		else
		{
			dataTracks++;
			Console.WriteLn(" * * Track %d: Data (Mode %d) (%d sectors)", i,((td.type==CDVD_MODE1_TRACK)?1:2),tlength);
		}
	}

	if (dataTracks > 0)
	{
		iCDType=CheckDiskTypeFS(iCDType);
	}

	if (audioTracks > 0)
	{
		switch (iCDType)
		{
			case CDVD_TYPE_PS2CD:
				iCDType=CDVD_TYPE_PS2CDDA;
				break;
			case CDVD_TYPE_PSCD:
				iCDType=CDVD_TYPE_PSCDDA;
				break;
			default:
				iCDType=CDVD_TYPE_CDDA;
				break;
		}
	}

	return iCDType;
}

static void DetectDiskType()
{
	if (CDVD->getTrayStatus() == CDVD_TRAY_OPEN)
	{
		diskTypeCached = CDVD_TYPE_NODISC;
		return;
	}

	int baseMediaType = CDVD->getDiskType();
	int mType = -1;

	// Paranoid mode: do not trust the plugin's detection system to work correctly.
	// (.. and there's no reason plugins should be doing their own detection anyway).

	switch(baseMediaType)
	{
#if 0
		case CDVD_TYPE_CDDA:
		case CDVD_TYPE_PSCD:
		case CDVD_TYPE_PS2CD:
		case CDVD_TYPE_PSCDDA:
		case CDVD_TYPE_PS2CDDA:
			mType = CDVD_TYPE_DETCTCD;
			break;

		case CDVD_TYPE_DVDV:
		case CDVD_TYPE_PS2DVD:
			mType = CDVD_TYPE_DETCTDVDS;
			break;

		case CDVD_TYPE_DETCTDVDS:
		case CDVD_TYPE_DETCTDVDD:
		case CDVD_TYPE_DETCTCD:
			mType = baseMediaType;
			break;
#endif

		case CDVD_TYPE_NODISC:
			diskTypeCached = CDVD_TYPE_NODISC;
			return;
	}

	diskTypeCached = FindDiskType(mType);
}

static wxString			m_SourceFilename[3];
static CDVD_SourceType	m_CurrentSourceType = CDVD_SourceType::NoDisc;

void CDVDsys_SetFile( CDVD_SourceType srctype, const wxString& newfile )
{
	m_SourceFilename[enum_cast(srctype)] = newfile;

	// look for symbol file
	if (symbolMap.IsEmpty())
	{
		wxString symName;
		int n = newfile.Last('.');
		if (n == wxNOT_FOUND)
			symName = newfile + L".sym";
		else
			symName = newfile.substr(0,n) + L".sym";

		wxCharBuffer buf = symName.ToUTF8();
		symbolMap.LoadNocashSym(buf);
		symbolMap.UpdateActiveSymbols();
	}
}

const wxString& CDVDsys_GetFile( CDVD_SourceType srctype )
{
	return m_SourceFilename[enum_cast(srctype)];
}

CDVD_SourceType CDVDsys_GetSourceType()
{
	return m_CurrentSourceType;
}

void CDVDsys_ChangeSource( CDVD_SourceType type )
{
	GetCorePlugins().Close( PluginId_CDVD );
	
	switch( m_CurrentSourceType = type )
	{
		case CDVD_SourceType::Iso:
			CDVD = &CDVDapi_Iso;
		break;

		case CDVD_SourceType::NoDisc:
			CDVD = &CDVDapi_NoDisc;
		break;

		case CDVD_SourceType::Plugin:
			CDVD = &CDVDapi_Plugin;
		break;

		jNO_DEFAULT;
	}
}

bool DoCDVDopen()
{
	CheckNullCDVD();

	// the new disk callback is set on Init also, but just in case the plugin clears it for
	// some reason on close, we re-send here:
	CDVD->newDiskCB( cdvdNewDiskCB );

	// Win32 Fail: the old CDVD api expects MBCS on Win32 platforms, but generating a MBCS
	// from unicode is problematic since we need to know the codepage of the text being
	// converted (which isn't really practical knowledge).  A 'best guess' would be the
	// default codepage of the user's Windows install, but even that will fail and return
	// question marks if the filename is another language.
	// Likely Fix: Force new versions of CDVD plugins to expect UTF8 instead.

	auto CurrentSourceType = enum_cast(m_CurrentSourceType);
	int ret = CDVD->open( !m_SourceFilename[CurrentSourceType].IsEmpty() ?
		static_cast<const char*>(m_SourceFilename[CurrentSourceType].ToUTF8()) : (char*)NULL
	);

	if( ret == -1 ) return false;	// error! (handled by caller)
	if( ret == 1 )	throw Exception::CancelEvent(L"User canceled the CDVD plugin's open dialog.");

	int cdtype = DoCDVDdetectDiskType();

	if (!EmuConfig.CdvdDumpBlocks || (cdtype == CDVD_TYPE_NODISC))
	{
		blockDumpFile.Close();
		return true;
	}

	wxString somepick( Path::GetFilenameWithoutExt( m_SourceFilename[CurrentSourceType] )  );
	//FWIW Disc serial availability doesn't seem reliable enough, sometimes it's there and sometime it's just null
	//Shouldn't the serial be available all time? Potentially need to look into Elfreloadinfo() reliability
	//TODO: Add extra fallback case for CRC.
	if (somepick.IsEmpty() && !DiscSerial.IsEmpty())
		somepick = L"Untitled-" + DiscSerial;
	else if (somepick.IsEmpty())
		somepick = L"Untitled";

	if (g_Conf->CurrentBlockdump.IsEmpty())
		g_Conf->CurrentBlockdump = wxGetCwd();

	wxString temp(Path::Combine(g_Conf->CurrentBlockdump, somepick));

#ifdef ENABLE_TIMESTAMPS
	wxDateTime curtime( wxDateTime::GetTimeNow() );

	temp += pxsFmt( L" (%04d-%02d-%02d %02d-%02d-%02d)",
		curtime.GetYear(), curtime.GetMonth(), curtime.GetDay(),
		curtime.GetHour(), curtime.GetMinute(), curtime.GetSecond()
	);
#endif
	temp += L".dump";

	cdvdTD td;
	CDVD->getTD(0, &td);

	blockDumpFile.Create(temp, 2);

	if( blockDumpFile.IsOpened() )
	{
		int  blockofs	= 0;
		uint blocksize	= CD_FRAMESIZE_RAW;
		uint blocks		= td.lsn;

		// hack: Because of limitations of the current cdvd design, we can't query the blocksize
		// of the underlying media.  So lets make a best guess:

		switch(cdtype)
		{
			case CDVD_TYPE_PS2DVD:
			case CDVD_TYPE_DVDV:
			case CDVD_TYPE_DETCTDVDS:
			case CDVD_TYPE_DETCTDVDD:
				blocksize = 2048;
			break;
		}
		blockDumpFile.WriteHeader(blockofs, blocksize, blocks);
	}


	return true;
}

void DoCDVDclose()
{
	CheckNullCDVD();
	//blockDumpFile.Close();

	if( CDVD->close != NULL )
		CDVD->close();

	DoCDVDresetDiskTypeCache();
}

s32 DoCDVDreadSector(u8* buffer, u32 lsn, int mode)
{
	CheckNullCDVD();
	int ret = CDVD->readSector(buffer,lsn,mode);

	if (ret == 0 && blockDumpFile.IsOpened())
	{
		if (blockDumpFile.GetBlockSize() == CD_FRAMESIZE_RAW && mode != CDVD_MODE_2352)
		{
			u8 blockDumpBuffer[CD_FRAMESIZE_RAW];
			if (CDVD->readSector(blockDumpBuffer, lsn, CDVD_MODE_2352) == 0)
				blockDumpFile.WriteSector(blockDumpBuffer, lsn);
		}
		else
		{
			blockDumpFile.WriteSector(buffer, lsn);
		}
	}

	return ret;
}

s32 DoCDVDreadTrack(u32 lsn, int mode)
{
	CheckNullCDVD();

	// TEMP: until all the plugins use the new CDVDgetBuffer style
	switch (mode)
	{
	case CDVD_MODE_2352:
		lastReadSize = 2352;
		break;
	case CDVD_MODE_2340:
		lastReadSize = 2340;
		break;
	case CDVD_MODE_2328:
		lastReadSize = 2328;
		break;
	case CDVD_MODE_2048:
		lastReadSize = 2048;
		break;
	}

	//DevCon.Warning("CDVD readTrack(lsn=%d,mode=%d)",params lsn, lastReadSize);
	lastLSN = lsn;
	return CDVD->readTrack(lsn,mode);
}

s32 DoCDVDgetBuffer(u8* buffer)
{
	CheckNullCDVD();
	int ret = CDVD->getBuffer2(buffer);

	if (ret == 0 && blockDumpFile.IsOpened())
	{
		cdvdTD td;
		CDVD->getTD(0, &td);

		if (lastLSN >= td.lsn)
			return 0;

		if (blockDumpFile.GetBlockSize() == CD_FRAMESIZE_RAW && lastReadSize != 2352)
		{
			u8 blockDumpBuffer[CD_FRAMESIZE_RAW];
			if (CDVD->readSector(blockDumpBuffer, lastLSN, CDVD_MODE_2352) == 0)
				blockDumpFile.WriteSector(blockDumpBuffer, lastLSN);
		}
		else
		{
			blockDumpFile.WriteSector(buffer, lastLSN);
		}
	}

	return ret;
}

s32 DoCDVDdetectDiskType()
{
	CheckNullCDVD();
	if(diskTypeCached < 0) DetectDiskType();
	return diskTypeCached;
}

void DoCDVDresetDiskTypeCache()
{
	diskTypeCached = -1;
}

////////////////////////////////////////////////////////
//
// CDVD null interface for Run BIOS menu



s32 CALLBACK NODISCopen(const char* pTitle)
{
	return 0;
}

void CALLBACK NODISCclose()
{
}

s32 CALLBACK NODISCreadTrack(u32 lsn, int mode)
{
	return -1;
}

// return can be NULL (for async modes)
u8* CALLBACK NODISCgetBuffer()
{
	return NULL;
}

s32 CALLBACK NODISCreadSubQ(u32 lsn, cdvdSubQ* subq)
{
	return -1;
}

s32 CALLBACK NODISCgetTN(cdvdTN *Buffer)
{
	return -1;
}

s32 CALLBACK NODISCgetTD(u8 Track, cdvdTD *Buffer)
{
	return -1;
}

s32 CALLBACK NODISCgetTOC(void* toc)
{
	return -1;
}

s32 CALLBACK NODISCgetDiskType()
{
	return CDVD_TYPE_NODISC;
}

s32 CALLBACK NODISCgetTrayStatus()
{
	return CDVD_TRAY_CLOSE;
}

s32 CALLBACK NODISCdummyS32()
{
	return 0;
}

void CALLBACK NODISCnewDiskCB(void (* /* callback */)())
{
}

s32 CALLBACK NODISCreadSector(u8* tempbuffer, u32 lsn, int mode)
{
	return -1;
}

s32 CALLBACK NODISCgetBuffer2(u8* buffer)
{
	return -1;
}

s32 CALLBACK NODISCgetDualInfo(s32* dualType, u32* _layer1start)
{
	return -1;
}

CDVD_API CDVDapi_NoDisc =
{
	NODISCclose,
	NODISCopen,
	NODISCreadTrack,
	NODISCgetBuffer,
	NODISCreadSubQ,
	NODISCgetTN,
	NODISCgetTD,
	NODISCgetTOC,
	NODISCgetDiskType,
	NODISCgetTrayStatus,
	NODISCdummyS32,
	NODISCdummyS32,

	NODISCnewDiskCB,

	NODISCreadSector,
	NODISCgetBuffer2,
	NODISCgetDualInfo,
};
