/* SPU2-X, A plugin for Emulating the Sound Processing Unit of the Playstation 2
 * Developed and maintained by the Pcsx2 Development Team.
 *
 * Original portions from SPU2ghz are (c) 2008 by David Quintana [gigaherz]
 *
 * SPU2-X 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.
 *
 * SPU2-X 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with SPU2-X.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "Global.h"
#include "PS2E-spu2.h"
#include "Dma.h"
#include "Dialogs.h"

#include "svnrev.h"

#ifdef _MSC_VER
#define snprintf sprintf_s
#endif
// PCSX2 expects ASNI, not unicode, so this MUST always be char...
static char libraryName[256];


static bool IsOpened		= false;
static bool IsInitialized	= false;

static u32  pClocks = 0;

u32*	cyclePtr	= NULL;
u32		lClocks		= 0;

#ifdef _MSC_VER
HINSTANCE hInstance;
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpvReserved)
{
	if( dwReason == DLL_PROCESS_ATTACH )
	{
		hInstance = hinstDLL;
	}
	else if( dwReason == DLL_PROCESS_DETACH )
	{
		// TODO : perform shutdown procedure, just in case PCSX2 itself failed
		// to for some reason..
	}
	return TRUE;
}
#endif

static void InitLibraryName()
{
#ifdef SPU2X_PUBLIC_RELEASE

	// Public Release!
	// Output a simplified string that's just our name:

	strcpy( libraryName, "SPU2-X" );

#else
	#ifdef SVN_REV_UNKNOWN

	// Unknown revision.
	// Output a name that includes devbuild status but not
	// subversion revision tags:

	strcpy( libraryName, "SPU2-X"
	#ifdef DEBUG_FAST
		"-Debug"
	#elif defined( PCSX2_DEBUG )
		"-Debug/Strict"		// strict debugging is slow!
	#elif defined( PCSX2_DEVBUILD )
		"-Dev"
	#else
		""
	#endif
	);

	#else

	// Use TortoiseSVN's SubWCRev utility's output
	// to label the specific revision:

	snprintf( libraryName, 255, "SPU2-X %lld%s"
	#ifdef DEBUG_FAST
		"-Debug"
	#elif defined( PCSX2_DEBUG )
		"-Debug/Strict"		// strict debugging is slow!
	#elif defined( PCSX2_DEVBUILD )
		"-Dev"
	#else
		""
	#endif
		,SVN_REV, SVN_MODS ? "m" : ""
	);
	#endif
#endif

}

//static bool cpu_detected = false;

static bool CheckSSE()
{
	return true;

	#if 0
	if( !cpu_detected )
	{
		cpudetectInit();
		cpu_detected = true;
	}
	if( !x86caps.hasStreamingSIMDExtensions || !x86caps.hasStreamingSIMD2Extensions )
	{
		SysMessage( "Your CPU does not support SSE2 instructions.\nThe SPU2-X plugin requires SSE2 to run." );
		return false;
	}
	return true;
	#endif
}

EXPORT_C_(u32) PS2EgetLibType()
{
	return PS2E_LT_SPU2;
}

EXPORT_C_(char*) PS2EgetLibName()
{
	InitLibraryName();
	return libraryName;
}

EXPORT_C_(u32) PS2EgetLibVersion2(u32 type)
{
	return (PS2E_SPU2_VERSION<<16) | (VersionInfo::Release<<8) | VersionInfo::Revision;
}

EXPORT_C_(void) SPU2configure()
{
	if( !CheckSSE() ) return;
	configure();
}

EXPORT_C_(void) SPU2about()
{
	AboutBox();
}

EXPORT_C_(s32) SPU2test()
{
	if( !CheckSSE() ) return -1;

	ReadSettings();
	if( SndBuffer::Test() != 0 )
	{
		// TODO : Implement a proper dialog that allows the user to test different audio out drivers.
		const wchar_t* wtf = mods[OutputModule]->GetIdent();
		SysMessage( L"The '%s' driver test failed.  Please configure\na different SoundOut module and try again.", wtf );
		return -1;
	}

	return 0;
}

// --------------------------------------------------------------------------------------
//  DMA 4/7 Callbacks from Core Emulator
// --------------------------------------------------------------------------------------

u16* DMABaseAddr;
void (* _irqcallback)();
void (* dma4callback)();
void (* dma7callback)();

EXPORT_C_(u32) CALLBACK SPU2ReadMemAddr(int core)
{
	return Cores[core].MADR;
}
EXPORT_C_(void) CALLBACK SPU2WriteMemAddr(int core,u32 value)
{
	Cores[core].MADR = value;
}

EXPORT_C_(void) CALLBACK SPU2setDMABaseAddr(uptr baseaddr)
{
   DMABaseAddr = (u16*)baseaddr;
}

EXPORT_C_(void) CALLBACK SPU2setSettingsDir(const char* dir)
{
	CfgSetSettingsDir( dir );
}

EXPORT_C_(void) CALLBACK SPU2setLogDir(const char* dir)
{
	CfgSetLogDir( dir );
}

EXPORT_C_(s32)  SPU2dmaRead(s32 channel, u32* data, u32 bytesLeft, u32* bytesProcessed)
{
	if(channel==4)
		return Cores[0].NewDmaRead(data,bytesLeft, bytesProcessed);
	else
		return Cores[1].NewDmaRead(data,bytesLeft, bytesProcessed);
}

EXPORT_C_(s32)  SPU2dmaWrite(s32 channel, u32* data, u32 bytesLeft, u32* bytesProcessed)
{
	if(channel==4)
		return Cores[0].NewDmaWrite(data,bytesLeft, bytesProcessed);
	else
		return Cores[1].NewDmaWrite(data,bytesLeft, bytesProcessed);
}

EXPORT_C_(void) SPU2dmaInterrupt(s32 channel)
{
	if(channel==4)
		return Cores[0].NewDmaInterrupt();
	else
		return Cores[1].NewDmaInterrupt();
}

#ifdef ENABLE_NEW_IOPDMA_SPU2
EXPORT_C_(void) SPU2irqCallback(void (*SPU2callback)())
{
	_irqcallback = SPU2callback;
}
#else
EXPORT_C_(void) SPU2irqCallback(void (*SPU2callback)(),void (*DMA4callback)(),void (*DMA7callback)())
{
	_irqcallback = SPU2callback;
	dma4callback = DMA4callback;
	dma7callback = DMA7callback;
}
#endif

EXPORT_C_(void) CALLBACK SPU2readDMA4Mem(u16 *pMem, u32 size)	// size now in 16bit units
{
	if( cyclePtr != NULL ) TimeUpdate( *cyclePtr );

	FileLog("[%10d] SPU2 readDMA4Mem size %x\n",Cycles, size<<1);
	Cores[0].DoDMAread(pMem, size);
}

EXPORT_C_(void) CALLBACK SPU2writeDMA4Mem(u16* pMem, u32 size)	// size now in 16bit units
{
	if( cyclePtr != NULL ) TimeUpdate( *cyclePtr );

	FileLog("[%10d] SPU2 writeDMA4Mem size %x at address %x\n",Cycles, size<<1, Cores[0].TSA);
#ifdef S2R_ENABLE
	if(!replay_mode)
		s2r_writedma4(Cycles,pMem,size);
#endif
	Cores[0].DoDMAwrite(pMem,size);
}

EXPORT_C_(void) CALLBACK SPU2interruptDMA4()
{
	FileLog("[%10d] SPU2 interruptDMA4\n",Cycles);
	Cores[0].Regs.STATX |= 0x80;
	//Cores[0].Regs.ATTR &= ~0x30;
}

EXPORT_C_(void) CALLBACK SPU2interruptDMA7()
{
	FileLog("[%10d] SPU2 interruptDMA7\n",Cycles);
	Cores[1].Regs.STATX |= 0x80;
	//Cores[1].Regs.ATTR &= ~0x30;
}

EXPORT_C_(void) CALLBACK SPU2readDMA7Mem(u16* pMem, u32 size)
{
	if( cyclePtr != NULL ) TimeUpdate( *cyclePtr );

	FileLog("[%10d] SPU2 readDMA7Mem size %x\n",Cycles, size<<1);
	Cores[1].DoDMAread(pMem,size);
}

EXPORT_C_(void) CALLBACK SPU2writeDMA7Mem(u16* pMem, u32 size)
{
	if( cyclePtr != NULL ) TimeUpdate( *cyclePtr );

	FileLog("[%10d] SPU2 writeDMA7Mem size %x at address %x\n",Cycles, size<<1, Cores[1].TSA);
#ifdef S2R_ENABLE
	if(!replay_mode)
		s2r_writedma7(Cycles,pMem,size);
#endif
	Cores[1].DoDMAwrite(pMem,size);
}

EXPORT_C_(void) SPU2reset()
{
	memset(spu2regs, 0, 0x010000);
	memset(_spu2mem, 0, 0x200000);
	memset(_spu2mem + 0x2800, 7, 0x10); // from BIOS reversal. Locks the voices so they don't run free.
	Cores[0].Init(0);
	Cores[1].Init(1);
}

EXPORT_C_(s32) SPU2init()
{
	assert( regtable[0x400] == NULL );

	if (IsInitialized)
	{
		printf( " * SPU2-X: Already initialized - Ignoring SPU2init signal." );
		return 0;
	}

	IsInitialized = true;

	ReadSettings();

#ifdef SPU2_LOG
	if(AccessLog())
	{
		spu2Log = OpenLog( AccessLogFileName );
		setvbuf(spu2Log, NULL,  _IONBF, 0);
		FileLog("SPU2init\n");
	}
#endif
	srand((unsigned)time(NULL));

	spu2regs  = (s16*)malloc(0x010000);
	_spu2mem  = (s16*)malloc(0x200000);

	// adpcm decoder cache:
	//  the cache data size is determined by taking the number of adpcm blocks
	//  (2MB / 16) and multiplying it by the decoded block size (28 samples).
	//  Thus: pcm_cache_data = 7,340,032 bytes (ouch!)
	//  Expanded: 16 bytes expands to 56 bytes [3.5:1 ratio]
	//    Resulting in 2MB * 3.5.

	pcm_cache_data = (PcmCacheEntry*)calloc( pcm_BlockCount, sizeof(PcmCacheEntry) );

	if( (spu2regs == NULL) || (_spu2mem == NULL) || (pcm_cache_data == NULL) )
	{
		SysMessage("SPU2-X: Error allocating Memory\n"); return -1;
	}
	
	// Patch up a copy of regtable that directly maps "NULLs" to SPU2 memory.
	
	memcpy(regtable, regtable_original, sizeof(regtable));

	for(uint mem=0;mem<0x800;mem++)
	{
		u16 *ptr = regtable[mem>>1];
		if(!ptr) {
			regtable[mem>>1] = &(spu2Ru16(mem));
		}
	}

	SPU2reset();

	DMALogOpen();
	InitADSR();

#ifdef S2R_ENABLE
	if(!replay_mode)
		s2r_open(Cycles,"replay_dump.s2r");
#endif
	return 0;
}

#ifdef _MSC_VER
// Bit ugly to have this here instead of in RealttimeDebugger.cpp, but meh :p
extern bool debugDialogOpen;
extern HWND hDebugDialog;

static BOOL CALLBACK DebugProc(HWND hWnd,UINT uMsg,WPARAM wParam,LPARAM lParam)
{
	int wmId;

	switch(uMsg)
	{
		case WM_PAINT:
			return FALSE;
		case WM_INITDIALOG:
			{
				debugDialogOpen=true;
			}
		break;

		case WM_COMMAND:
			wmId    = LOWORD(wParam);
			// Parse the menu selections:
			switch (wmId)
			{
				case IDOK:
				case IDCANCEL:
					debugDialogOpen=false;
					EndDialog(hWnd,0);
					break;
				default:
					return FALSE;
			}
		break;

		default:
			return FALSE;
	}
	return TRUE;
}
#endif
uptr gsWindowHandle = 0;

EXPORT_C_(s32) SPU2open(void *pDsp)
{
	if( IsOpened ) return 0;

	FileLog("[%10d] SPU2 Open\n",Cycles);

	if( pDsp != NULL )
		gsWindowHandle = *(uptr*)pDsp;
	else
		gsWindowHandle = 0;

#ifdef _MSC_VER
#ifdef PCSX2_DEVBUILD // Define may not be needed but not tested yet. Better make sure.
	if( IsDevBuild && VisualDebug() ) 
	{
		if(debugDialogOpen==0)
		{
			hDebugDialog = CreateDialogParam(hInstance,MAKEINTRESOURCE(IDD_DEBUG),0,DebugProc,0);
			ShowWindow(hDebugDialog,SW_SHOWNORMAL);
			debugDialogOpen=1;
		}
	}
	else if(debugDialogOpen) 
	{
		DestroyWindow(hDebugDialog);
		debugDialogOpen=0;
	}
#endif
#endif

	IsOpened = true;
	lClocks  = (cyclePtr!=NULL) ? *cyclePtr : 0;

	try
	{
		SndBuffer::Init();
		
#ifndef __linux__
		DspLoadLibrary(dspPlugin,dspPluginModule);
#endif
		WaveDump::Open();
	}
	catch( std::exception& ex )
	{
		fprintf( stderr, "SPU2-X Error: Could not initialize device, or something.\nReason: %s", ex.what() );
		SPU2close();
		return -1;
	}
	return 0;
}

EXPORT_C_(void) SPU2close()
{
	if( !IsOpened ) return;
	IsOpened = false;

	FileLog("[%10d] SPU2 Close\n",Cycles);

#ifndef __linux__
	DspCloseLibrary();
#endif

	SndBuffer::Cleanup();
}

EXPORT_C_(void) SPU2shutdown()
{
	if(!IsInitialized) return;
	IsInitialized = false;

	ConLog( "* SPU2-X: Shutting down.\n" );

	SPU2close();

#ifdef S2R_ENABLE
	if(!replay_mode)
		s2r_close();
#endif

	DoFullDump();
#ifdef STREAM_DUMP
	fclose(il0);
	fclose(il1);
#endif
#ifdef EFFECTS_DUMP
	fclose(el0);
	fclose(el1);
#endif
	WaveDump::Close();

	DMALogClose();

	safe_free(spu2regs);
	safe_free(_spu2mem);
	safe_free( pcm_cache_data );


#ifdef SPU2_LOG
	if(!AccessLog()) return;
	FileLog("[%10d] SPU2shutdown\n",Cycles);
	if(spu2Log) fclose(spu2Log);
#endif
}

EXPORT_C_(void) SPU2setClockPtr(u32 *ptr)
{
	cyclePtr = ptr;
}

#ifdef DEBUG_KEYS
static u32 lastTicks;
static bool lState[6];
#endif

EXPORT_C_(void) SPU2async(u32 cycles)
{
	DspUpdate();

	if(cyclePtr != NULL)
	{
		TimeUpdate( *cyclePtr );
	}
	else
	{
		pClocks += cycles;
		TimeUpdate( pClocks );
	}

#ifdef DEBUG_KEYS
	u32 curTicks = GetTickCount();
	if((curTicks - lastTicks) >= 50)
	{
		int oldI = Interpolation;
		bool cState[6];
		for(int i=0;i<6;i++)
		{
			cState[i] = !!(GetAsyncKeyState(VK_NUMPAD0+i)&0x8000);

			if((cState[i] && !lState[i]) && i != 5)
				Interpolation = i;

			if((cState[i] && !lState[i]) && i == 5)
			{
				postprocess_filter_enabled = !postprocess_filter_enabled;
				printf("Post process filters %s \n", postprocess_filter_enabled? "enabled" : "disabled");
			}

			lState[i] = cState[i];
		}

		if(Interpolation != oldI)
		{
			printf("Interpolation set to %d", Interpolation);
			switch(Interpolation)
			{
			case 0: printf(" - Nearest.\n"); break;
			case 1: printf(" - Linear.\n"); break;
			case 2: printf(" - Cubic.\n"); break;
			case 3: printf(" - Hermite.\n"); break;
			case 4: printf(" - Catmull-Rom.\n"); break;
			default: printf(" (unknown).\n"); break;
			}
		}

		lastTicks = curTicks;
	}
#endif
}

EXPORT_C_(u16) SPU2read(u32 rmem)
{
	//	if(!replay_mode)
	//		s2r_readreg(Cycles,rmem);

	u16 ret=0xDEAD; u32 core=0, mem=rmem&0xFFFF, omem=mem;
	if (mem & 0x400) { omem^=0x400; core=1; }

	if(omem == 0x1f9001AC)
	{
		ret = Cores[core].DmaRead();
	}
	else
	{
		if( cyclePtr != NULL )
			TimeUpdate( *cyclePtr );

		if (rmem>>16 == 0x1f80)
		{
			ret = Cores[0].ReadRegPS1(rmem);
		}
		else if (mem >= 0x800)
		{
			ret = spu2Ru16(mem);
			ConLog("* SPU2-X: Read from reg>=0x800: %x value %x\n",mem,ret);
		}
		else
		{
			ret = *(regtable[(mem>>1)]);
			//FileLog("[%10d] SPU2 read mem %x (core %d, register %x): %x\n",Cycles, mem, core, (omem & 0x7ff), ret);
			SPU2writeLog( "read", rmem, ret );
		}
	}

	return ret;
}

EXPORT_C_(void) SPU2write(u32 rmem, u16 value)
{
#ifdef S2R_ENABLE
	if(!replay_mode)
		s2r_writereg(Cycles,rmem,value);
#endif

	// Note: Reverb/Effects are very sensitive to having precise update timings.
	// If the SPU2 isn't in in sync with the IOP, samples can end up playing at rather
	// incorrect pitches and loop lengths.

	if( cyclePtr != NULL )
		TimeUpdate( *cyclePtr );

	if (rmem>>16 == 0x1f80)
		Cores[0].WriteRegPS1(rmem,value);
	else
	{
		SPU2writeLog( "write", rmem, value );
		SPU2_FastWrite( rmem, value );
	}
}

// if start is 1, starts recording spu2 data, else stops
// returns a non zero value if successful
// for now, pData is not used
EXPORT_C_(int) SPU2setupRecording(int start, void* pData)
{
	if(start==0)
		RecordStop();
	else if(start==1)
		RecordStart();

	return 0;
}

EXPORT_C_(s32) SPU2freeze(int mode, freezeData *data)
{
	jASSUME( data != NULL );
	if ( !data )
	{
		printf("SPU2-X savestate null pointer!\n");
		return -1;
	}

	if( mode == FREEZE_SIZE )
	{
		data->size = Savestate::SizeIt();
		return 0;
	}

	jASSUME( mode == FREEZE_LOAD || mode == FREEZE_SAVE );

	if( data->data == NULL )
	{
		printf("SPU2-X savestate null pointer!\n");
		return -1;
	}

	Savestate::DataBlock& spud = (Savestate::DataBlock&)*(data->data);

	switch( mode )
	{
		case FREEZE_LOAD: return Savestate::ThawIt( spud );
		case FREEZE_SAVE: return Savestate::FreezeIt( spud );

		jNO_DEFAULT;
	}

	// technically unreachable, but kills a warning:
	return 0;
}
