/*  ZZ Open GL graphics plugin
 *  Copyright (c)2009-2010 zeydlitz@gmail.com, arcum42@gmail.com
 *  Based on Zerofrog's ZeroGS KOSMOS (c)2005-2008
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program 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 this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 */

#include "Util.h"
#include "GS.h"
#include "Profile.h"
#include "GLWin.h"
#include "ZZoglFlushHack.h"
#include "ZZoglShaders.h"


using namespace std;

extern void SaveSnapshot(const char* filename);

#ifdef _MSC_VER
#pragma warning(disable:4244)
#endif

GLWindow GLWin;
GSinternal gs;
GSconf conf;
GSDump g_dump;

int ppf, g_GSMultiThreaded, CurrentSavestate = 0;
int g_LastCRC = 0, g_TransferredToGPU = 0, s_frameskipping = 0;
int g_SkipFlushFrame = 0;
GetSkipCount GetSkipCount_Handler = 0;

int UPDATE_FRAMES = 16, g_nFrame = 0, g_nRealFrame = 0;
float fFPS = 0;

void (*GSirq)();
u8* g_pBasePS2Mem = NULL;
wxString s_strIniPath(L"inis");  	// Air's new ini path (r2361)

bool SaveStateExists = true;		// We could not know save slot status before first change occured
const char* SaveStateFile = NULL;	// Name of SaveFile for access check.

extern const char* s_aa[5];
extern const char* pbilinear[];
// statistics
u32 g_nGenVars = 0, g_nTexVars = 0, g_nAlphaVars = 0, g_nResolve = 0;

#define VER 4
const unsigned char zgsversion	= PS2E_GS_VERSION;
unsigned char zgsrevision = 0; // revision and build gives plugin version
unsigned char zgsbuild	= VER;
unsigned char zgsminor = 0;

#ifdef _DEBUG
const char *libraryName	 = "ZZ Ogl PG (Debug) ";
#elif defined(ZEROGS_DEVBUILD)
const char *libraryName	 = "ZZ Ogl PG (Dev)";
#else
const char *libraryName	 = "ZZ Ogl PG ";
#endif

extern int g_nPixelShaderVer, g_nFrameRender, g_nFramesSkipped;

extern void WriteAA();
extern void WriteBilinear();
extern void ZZDestroy();
extern bool ZZCreate(int width, int height);
extern void ZZGSStateReset();
extern int ZZSave(s8* pbydata);
extern bool ZZLoad(s8* pbydata);

// switches the render target to the real target, flushes the current render targets and renders the real image
extern void RenderCRTC();

#if defined(_WIN32) && defined(_DEBUG)
HANDLE g_hCurrentThread = NULL;
#endif

extern int VALIDATE_THRESH;
extern u32 TEXDESTROY_THRESH;

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

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

EXPORT_C_(u32) PS2EgetLibVersion2(u32 type)
{
	return (zgsversion << 16) | (zgsrevision << 8) | zgsbuild | (zgsminor << 24);
}

EXPORT_C_(void) GSsetBaseMem(void* pmem)
{
	g_pBasePS2Mem = (u8*)pmem;
}

EXPORT_C_(void) GSsetSettingsDir(const char* dir)
{
	s_strIniPath = (dir == NULL) ? wxString(L"inis") : wxString(dir, wxConvFile);
}

EXPORT_C_(void) GSsetLogDir(const char* dir)
{
	ZZLog::SetDir(dir);
}

EXPORT_C_(void) GSsetGameCRC(int crc, int options)
{
    // build a list of function pointer for GetSkipCount (SkipDraw)
	static GetSkipCount GSC_list[NUMBER_OF_TITLES];
	static bool inited = false;
	
	if (!inited)
	{
		inited = true;

		memset(GSC_list, 0, sizeof(GSC_list));
		
		GSC_list[Okami] = GSC_Okami;
		GSC_list[MetalGearSolid3] = GSC_MetalGearSolid3;
		GSC_list[DBZBT2] = GSC_DBZBT2;
		GSC_list[DBZBT3] = GSC_DBZBT3;
		GSC_list[SFEX3] = GSC_SFEX3;
		GSC_list[Bully] = GSC_Bully;
		GSC_list[BullyCC] = GSC_BullyCC;
		GSC_list[SoTC] = GSC_SoTC;
		GSC_list[OnePieceGrandAdventure] = GSC_OnePieceGrandAdventure;
		GSC_list[OnePieceGrandBattle] = GSC_OnePieceGrandBattle;
		GSC_list[ICO] = GSC_ICO;
		GSC_list[GT4] = GSC_GT4;
		GSC_list[WildArms4] = GSC_WildArms4;
		GSC_list[WildArms5] = GSC_WildArms5;
		GSC_list[Manhunt2] = GSC_Manhunt2;
		GSC_list[CrashBandicootWoC] = GSC_CrashBandicootWoC;
		GSC_list[ResidentEvil4] = GSC_ResidentEvil4;
		GSC_list[Spartan] = GSC_Spartan;
		GSC_list[AceCombat4] = GSC_AceCombat4;
		GSC_list[Drakengard2] = GSC_Drakengard2;
		GSC_list[Tekken5] = GSC_Tekken5;
		GSC_list[IkkiTousen] = GSC_IkkiTousen;
		GSC_list[GodOfWar] = GSC_GodOfWar;
		GSC_list[GodOfWar2] = GSC_GodOfWar2;
		GSC_list[GiTS] = GSC_GiTS;
		GSC_list[Onimusha3] = GSC_Onimusha3;
		GSC_list[TalesOfAbyss] = GSC_TalesOfAbyss;
		GSC_list[SonicUnleashed] = GSC_SonicUnleashed;
		GSC_list[Genji] = GSC_Genji;
		GSC_list[StarOcean3] = GSC_StarOcean3;
		GSC_list[ValkyrieProfile2] = GSC_ValkyrieProfile2;
		GSC_list[RadiataStories] = GSC_RadiataStories;
	}

	// TEXDESTROY_THRESH starts out at 16.
	VALIDATE_THRESH = 8;
	conf.mrtdepth = (conf.settings().disable_mrt_depth != 0);

	if (!conf.mrtdepth)
		ZZLog::WriteLn("Disabling MRT depth writing.");
	else
		ZZLog::WriteLn("Enabling MRT depth writing.");

	bool CRCValueChanged = (g_LastCRC != crc);

	g_LastCRC = crc;

	if (crc != 0) ZZLog::WriteLn("Current game CRC is %x.", crc);

	if (CRCValueChanged && (crc != 0))
	{
		for (u32 i = 0; i < GAME_INFO_INDEX; i++)
		{
			if (crc_game_list[i].crc == (u32)crc)
			{
				ZZLog::WriteLn("Found CRC[%x] in crc game list.", crc);
				
				if (crc_game_list[i].v_thresh > 0) 
				{
					VALIDATE_THRESH = crc_game_list[i].v_thresh;
					ZZLog::WriteLn("Setting VALIDATE_THRESH to %d", VALIDATE_THRESH);
				}
				
				if (crc_game_list[i].t_thresh > 0) 
				{
					TEXDESTROY_THRESH = crc_game_list[i].t_thresh;
					ZZLog::WriteLn("Setting TEXDESTROY_THRESH to %d", TEXDESTROY_THRESH);
				}

                // FIXME need to check SkipDraw is positive (enabled by users)
                GetSkipCount_Handler = GSC_list[crc_game_list[i].title];

				if (!conf.disableHacks) 
				{
					conf.def_hacks._u32 |= crc_game_list[i].flags;
					ListHacks();
				}
				return;
			}
		}
	}
	ListHacks();
}

EXPORT_C_(void) GSsetFrameSkip(int frameskip)
{
	FUNCLOG
	s_frameskipping |= frameskip;

	if (frameskip && g_nFrameRender > 1)
	{
		SetFrameSkip(true);
	}
	else if (!frameskip && g_nFrameRender <= 0)
	{
		SetFrameSkip(false);
	}
}

EXPORT_C_(void) GSreset()
{
	FUNCLOG

	memset(&gs, 0, sizeof(gs));

	ZZGSStateReset();

	gs.prac = 1;
	prim = &gs._prim[0];
	gs.transferring = false;
	gs.q = 1;
}

EXPORT_C_(void) GSgifSoftReset(u32 mask)
{
	FUNCLOG

	if (mask & 1) memset(&gs.path[0], 0, sizeof(gs.path[0]));
	if (mask & 2) memset(&gs.path[1], 0, sizeof(gs.path[1]));
	if (mask & 4) memset(&gs.path[2], 0, sizeof(gs.path[2]));

	gs.transferring = false;
	gs.q = 1;
}

EXPORT_C_(s32) GSinit()
{
	FUNCLOG

    ZZLog::Open();
	ZZLog::WriteLn("Calling GSinit.");

	WriteTempRegs();
	GSreset();
	
	ZZLog::WriteLn("GSinit finished.");
	return 0;
}

__forceinline void InitMisc()
{
	WriteBilinear();
	WriteAA();
	InitProfile();
	InitPath();
	ResetRegs();
}

EXPORT_C_(s32) GSopen(void *pDsp, char *Title, int multithread)
{
	FUNCLOG

	g_GSMultiThreaded = multithread;

	ZZLog::WriteLn("Calling GSopen.");

#if defined(_WIN32) && defined(_DEBUG)
	g_hCurrentThread = GetCurrentThread();
#endif

	LoadConfig();
	strcpy(GLWin.title, Title);

	ZZLog::GS_Log("Using %s:%d.%d.%d.", libraryName, zgsrevision, zgsbuild, zgsminor);
	
	ZZLog::WriteLn("Creating ZZOgl window.");
	if ((!GLWin.CreateWindow(pDsp)) || (!ZZCreate(conf.width, conf.height))) return -1;

	ZZLog::WriteLn("Initialization successful.");

	InitMisc();
	ZZLog::GS_Log("GSopen finished.");
	return 0;
}

#ifdef USE_GSOPEN2
EXPORT_C_(s32) GSopen2( void* pDsp, u32 flags )
{
	FUNCLOG

	g_GSMultiThreaded = true;

	ZZLog::WriteLn("Calling GSopen2.");

#if defined(_WIN32) && defined(_DEBUG)
	g_hCurrentThread = GetCurrentThread();
#endif

	LoadConfig();
	
	ZZLog::GS_Log("Using %s:%d.%d.%d.", libraryName, zgsrevision, zgsbuild, zgsminor);

	ZZLog::WriteLn("Capturing ZZOgl window.");
	if ((!GLWin.CreateWindow(pDsp)) || (!ZZCreate(conf.width, conf.height))) return -1;

	ZZLog::WriteLn("Initialization successful.");

	InitMisc();
	ZZLog::GS_Log("GSopen2 finished.");
	ZZLog::Check_GL_Error();
	return 0;
	
}
#endif

EXPORT_C_(void) GSshutdown()
{
	FUNCLOG

	ZZLog::Close();
}

EXPORT_C_(void) GSclose()
{
	FUNCLOG

	// Clean shader. Must be done before the context is delete
	ZZshExitCleaning();
	ZZDestroy();

	GLWin.CloseWindow();

	// Free alocated memory. We could close plugin without closing pcsx2, so we SHOULD free all allocated resources
	SaveStateFile = NULL;
	SaveStateExists = true; // default value
    g_LastCRC = 0;
}

EXPORT_C_(void) GSirqCallback(void (*callback)())
{
	FUNCLOG

	GSirq = callback;
}

EXPORT_C_(void) GSwriteCSR(u32 write)
{
	FUNCLOG

	gs.CSRw = write;
}

#ifdef _WIN32
#define access _access
#endif

EXPORT_C_(void) GSchangeSaveState(int newstate, const char* filename)
{
	FUNCLOG

	char str[255];
	sprintf(str, "save state %d", newstate);
	ZZAddMessage(str);
	CurrentSavestate = newstate;

	SaveStateFile = filename;
	SaveStateExists = (access(SaveStateFile, 0) == 0);
}

static bool get_snapshot_filename(char *filename, const char* path, const char* extension)
{
	FUNCLOG

	FILE *bmpfile;
	u32 snapshotnr = 0;

	// increment snapshot value & try to get filename

	for (;;)
	{
		snapshotnr++;

		sprintf(filename, "%s/snap%03d.%s", path, snapshotnr, extension);

		bmpfile = fopen(filename, "rb");

		if (bmpfile == NULL) break;

		fclose(bmpfile);
	}

	// try opening new snapshot file
	if ((bmpfile = fopen(filename, "wb")) == NULL)
	{
		char strdir[255];
		sprintf(strdir, "%s", path);

#ifdef _WIN32
		CreateDirectory(wxString::FromUTF8(strdir), NULL);
#else
		mkdir(path, 0777);
#endif

		if ((bmpfile = fopen(filename, "wb")) == NULL) return false;
	}

	fclose(bmpfile);

	return true;
}

EXPORT_C_(void) GSmakeSnapshot(char *path)
{
	FUNCLOG

	char filename[256];
	if (get_snapshot_filename(filename, (const char*)path, (conf.zz_options.tga_snap) ? "bmp" : "jpg"))
		SaveSnapshot(filename);
}

// I'll probably move this somewhere else later, but it's got a ton of dependencies.
static __forceinline void SetGSTitle()
{
	char strtitle[256];

#if !defined(ZEROGS_DEVBUILD)
	const char* g_pShaders[4] = { "full", "reduced", "accurate", "accurate-reduced" };
	const char* g_pInterlace[3] = { "interlace 0 |", "interlace 1 |", "" };
	const char* g_pBilinear[3] = { "", "bilinear |", "forced bilinear |" };

	if (SaveStateFile != NULL && !SaveStateExists)
		SaveStateExists = (access(SaveStateFile, 0) == 0);
	else
		SaveStateExists = true;

	sprintf(strtitle, "ZZ Open GL 0.%d.%d | %.1f fps | %s%s%s savestate %d%s | shaders %s | (%.1f)", zgsbuild, zgsminor, fFPS,
			g_pInterlace[conf.interlace], g_pBilinear[conf.bilinear], (conf.aa ? s_aa[conf.aa] : ""),
					CurrentSavestate, (SaveStateExists ? "" :  "*"),
					g_pShaders[g_nPixelShaderVer], (ppf&0xfffff) / (float)UPDATE_FRAMES);

#else
	sprintf(strtitle, "%d | %.1f fps (sk:%d%%) | g: %.1f, t: %.1f, a: %.1f, r: %.1f | p: %.1f | tex: %d %d (%d kbpf)", g_nFrame, fFPS,
			100*g_nFramesSkipped / g_nFrame,
			g_nGenVars / (float)UPDATE_FRAMES, g_nTexVars / (float)UPDATE_FRAMES, g_nAlphaVars / (float)UPDATE_FRAMES,
			g_nResolve / (float)UPDATE_FRAMES, (ppf&0xfffff) / (float)UPDATE_FRAMES,
			g_MemTargs.listTargets.size(), g_MemTargs.listClearedTargets.size(), g_TransferredToGPU >> 10);

	//_snprintf(strtitle, 512, "%x %x", *(int*)(g_pbyGSMemory + 256 * 0x3e0c + 4), *(int*)(g_pbyGSMemory + 256 * 0x3e04 + 4));
#endif

//	if( g_nFrame > 100 && fFPS > 60.0f ) {
//		ZZLog::Debug_Log("Set profile.");
//		g_bWriteProfile = 1;
//	}
	GLWin.SetTitle(strtitle);
}

// This isn't implemented for some reason? Adding a field for it for the moment, till I get a chance to look closer.
EXPORT_C_(void) GSsetVsync(int enabled)
{
	FUNCLOG

	ZZLog::Debug_Log("Setting VSync to 0x%x.", enabled);
	gs.vsync = enabled;
	GLWin.SetVsync(enabled);
}

EXPORT_C_(void) GSvsync(int current_interlace)
{
	FUNCLOG

	//ZZLog::GS_Log("Calling GSvsync.");

	static u32 dwTime = timeGetTime();
	static int nToNextUpdate = 1;
#ifdef _DEBUG
	if (conf.dump & 0x1) {
		freezeData fd;
		fd.size = ZZSave(NULL);
		s8* payload = (s8*)malloc(fd.size);
		fd.data = payload;
		 
		ZZSave(fd.data);

		char filename[256];
		// FIXME, there is probably a better solution than /tmp ...
		// A possibility will be to save the path from GSmakeSnapshot but you still need to call
		// GSmakeSnapshot first.
		if (get_snapshot_filename(filename, "/tmp", "gs"))
			g_dump.Open(filename, g_LastCRC, fd, g_pBasePS2Mem);
		conf.dump--;

		free(payload);
	}
	g_dump.VSync(current_interlace, (conf.dump == 0), g_pBasePS2Mem);
#endif

	GL_REPORT_ERRORD();

	g_nRealFrame++;

	// The value passed seems to either be 0 or 0x2000, and we want 0 or 1. Perhaps !! would be better...
	gs.interlace = !current_interlace;
	RenderCRTC();

	GLWin.ProcessEvents();

	if (--nToNextUpdate <= 0)
	{
		u32 d = timeGetTime();
		fFPS = UPDATE_FRAMES * 1000.0f / (float)max(d - dwTime, (u32)1);
		dwTime = d;
		g_nFrame += UPDATE_FRAMES;
#ifndef USE_GSOPEN2
		// let PCSX2 manage the title
		SetGSTitle();
#endif

//		if( g_nFrame > 100 && fFPS > 60.0f ) {
//			ZZLog::Debug_Log("Set profile.");
//			g_bWriteProfile = 1;
//		}

		if (fFPS < 16) 
			UPDATE_FRAMES = 4;
		else if (fFPS < 32) 
			UPDATE_FRAMES = 8;
		else 
			UPDATE_FRAMES = 16;

		nToNextUpdate = UPDATE_FRAMES;

		ppf = 0;
		g_TransferredToGPU = 0;
		g_nGenVars = 0;
		g_nTexVars = 0;
		g_nAlphaVars = 0;
		g_nResolve = 0;
		g_nFramesSkipped = 0;
        g_SkipFlushFrame = 0;
	}

#if defined(ZEROGS_DEVBUILD)
	if (g_bWriteProfile)
	{
		//g_bWriteProfile = 0;
		DVProfWrite("prof.txt", UPDATE_FRAMES);
		DVProfClear();
	}

#endif
	GL_REPORT_ERRORD();

}

EXPORT_C_(void) GSreadFIFO(u64 *pMem)
{
	FUNCLOG

	//ZZLog::GS_Log("Calling GSreadFIFO.");
#ifdef _DEBUG
	g_dump.ReadFIFO(1);
#endif

	TransferLocalHost((u32*)pMem, 1);
}

EXPORT_C_(void) GSreadFIFO2(u64 *pMem, int qwc)
{
	FUNCLOG

	//ZZLog::GS_Log("Calling GSreadFIFO2.");
#ifdef _DEBUG
	g_dump.ReadFIFO(qwc);
#endif

	TransferLocalHost((u32*)pMem, qwc);
}

EXPORT_C_(int) GSsetupRecording(int start, void* pData)
{
	FUNCLOG

	if (start)
		StartCapture();
	else
		StopCapture();

	return 1;
}

EXPORT_C_(s32) GSfreeze(int mode, freezeData *data)
{
	FUNCLOG

	switch (mode)
	{
		case FREEZE_LOAD:
			if (!ZZLoad(data->data)) ZZLog::Error_Log("GS: Bad load format!");
			g_nRealFrame += 100;
			break;

		case FREEZE_SAVE:
			ZZSave(data->data);
			break;

		case FREEZE_SIZE:
			data->size = ZZSave(NULL);
			break;

		default:
			break;
	}

	return 0;
}

#ifdef __linux__
void _fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
{
	static uint32 read_cnt = 0;
	read_cnt++;

	size_t result = fread(ptr, size, nmemb, stream);
	if (result != nmemb) {
		fprintf(stderr, "Read error\n");
		exit(read_cnt);
	}
}

struct Packet 
{
	u8 type, param;
	u32 size, addr;
	u32 real_size;
	vector<u32> buff;
};

EXPORT_C_(void) GSReplay(char* lpszCmdLine)
{
	if(FILE* fp = fopen(lpszCmdLine, "rb"))
	{
		GSinit();

		u8 regs[0x2000];
		GSsetBaseMem(regs);

		//s_vsync = !!theApp.GetConfig("vsync", 0);

		void* hWnd = NULL;

		const char* title = "replayer";
		GSopen((void**)&hWnd, (char*)title, 0);

		u32 crc;
		_fread(&crc, 4, 1, fp);
		GSsetGameCRC(crc, 0);

		freezeData fd;
		_fread(&fd.size, 4, 1, fp);
		fd.data = new s8[fd.size];
		_fread(fd.data, fd.size, 1, fp);
		GSfreeze(FREEZE_LOAD, &fd);
		delete [] fd.data;

		_fread(regs, 0x2000, 1, fp);

		GSvsync(1);

		list<Packet*> packets;
		vector<u8> buff;
		int type;

		while((type = fgetc(fp)) != EOF)
		{
			Packet* p = new Packet();

			p->type = (u8)type;

			switch(type)
			{
			case 0:

				p->param = (u8)fgetc(fp);

				_fread(&p->size, 4, 1, fp);
				_fread(&p->real_size, 4, 1, fp);

				switch(p->param)
				{
				case 0:
					p->buff.resize(0x4000);
					//p->addr = 0x4000 - p->size;
					//fread(&p->buff[p->addr], p->size, 1, fp);
					_fread(&p->buff[0], p->size, 1, fp);
					break;
				case 1:
				case 2:
				case 3:
					p->buff.resize(p->size);
					_fread(&p->buff[0], p->size, 1, fp);
					break;
				}

				break;

			case 1:

				_fread(&p->param, 4, 1, fp);
				//p->param = (u8)fgetc(fp);

				break;

			case 2:

				_fread(&p->size, 4, 1, fp);

				break;

			case 3:

				p->buff.resize(0x2000);

				_fread(&p->buff[0], 0x2000, 1, fp);

				break;

			default: assert(0);
			}

			packets.push_back(p);
		}

		sleep(1);

		//while(IsWindowVisible(hWnd))
		//FIXME map?
		int finished = 2;
		while(finished > 0)
		{
			unsigned long start = timeGetTime();
			unsigned long frame_number = 0;
			for(list<Packet*>::iterator i = packets.begin(); i != packets.end(); i++)
			{
				Packet* p = *i;

				switch(p->type)
				{
				case 0:

					//fprintf(stderr, "TRANSFER %d size %d\n", p->param, p->real_size);
					switch(p->param)
					{
					//case 0: GSgifTransfer1((u32*)&p->buff[0], p->addr); break;
					case 0: GSgifTransfer0((u32*)&p->buff[0], p->real_size); break;
					case 1: GSgifTransfer2((u32*)&p->buff[0], p->real_size); break;
					case 2: GSgifTransfer3((u32*)&p->buff[0], p->real_size); break;
					case 3: GSgifTransfer((u32*)&p->buff[0], p->real_size); break;
					}

					break;

				case 1:

					GSvsync(p->param);
					frame_number++;

					break;

				case 2:

					if(buff.size() < p->size) buff.resize(p->size);

					// FIXME
					// GSreadFIFO2(&buff[0], p->size / 16);
					fprintf(stderr, "GSreadFIFO2 not yet implemented");

					break;

				case 3:

					memcpy(regs, &p->buff[0], 0x2000);

					break;
				}
			}
			unsigned long end = timeGetTime();
			fprintf(stderr, "The %ld frames of the scene was render on %ldms\n", frame_number, end - start);
			fprintf(stderr, "A means of %fms by frame (limit 16ms/f)\n", (float)(end - start)/(float)frame_number);

			sleep(1);
			finished--;
		}


		for(list<Packet*>::iterator i = packets.begin(); i != packets.end(); i++)
		{
			delete *i;
		}

		packets.clear();

		sleep(1);

		GSclose();
		GSshutdown();

		fclose(fp);
	}
}
#endif
