/* Copyright 2009 WiiGator. */
#include "types.h"

#include "debugprintf.h"
#include "dvdreadpatch.h"
#include "writebranch.h"
#include "codecmp.h"
#include "cache.h"


/** Original DVD read function begins with the following bytes: */
static const u32 _Read_original[] = {
	0x7C0802A6,
	0x90010004,
	0x38000000,
	0x9421FFD8,
	0x93E10024,
	0x93C10020,
	0x3BC50000,
	0x93a1001c,
	0x3ba40000,
	0x93810018,
	0x3b830000,
	0x900d0c60,
	0x38000001,
	0x90cd0c68,
	0x0,
	0x0,
	0x900d0ca0,
	0x48017085,
	0x908d0c9c,
	0x3c80cc00,
	0x3c0000a0,
	0x906d0c98,
	0x38846000,
	0x3c60a800,
};

static const u32 _Read_original2[] = {
	0x7c0802a6,
	0x90010004,
	0x9421ffe0,
	0x93e1001c,
	0x90610008,
	0x7c9f2378,
	0x90a10010,
	0x90c10014,
	0x80010014,
	0x900da720,
	0x38000000,
	0x900da718,
	0x38000001,
	0x900da758,
};

static const u32 _Read_original3[] = {
	0x7c0802a6,
	0x90010004,
	0x38000001,
	0x9421ffd8,
	0x93e10024,
	0x93c10020,
	0x3bc50000,
	0x93a1001c,
	0x3ba40000,
	0x93810018,
	0x3b830000,
	0x90cd16bc,
	0x3cc0804d,
	0x3be6bdc0,
	0x900d16f8
};

#if defined(MIOS_PLUGIN) || defined(HIGH_PLUGIN) /* Action replay is only loaded by MIOS. */
#ifdef ACTION_REPLAY
static const u32 ar_dvd_high_read[] = {
	/* Freeloader, Action Relpay */
	0x9421ffe0,
	0x7c0802a6,
	0x9361000c,
	0x93810010,
	0x93a10014,
	0x93c10018,
	0x93e1001c,
	0x90010024,
	0x7c9c2378,
	0x7cbd2b78,
	0x7c7b1b78,
	0x4bfffebd,
	0x3d208130,
	0x3c00a800,
};

static const u32 ar_dvd_high_read_disk_id[] = {
	0x9421ffe8,
	0x7c0802a6,
	0x93a1000c,
	0x93c10010,
	0x93e10014,
	0x9001001c,
	0x7c7d1b78,
	0x4bffffb9,
	0x3c00a800,
	0x0,
	0x60000040,
};
#endif
#endif

#ifdef GEKKO_DEBUG
static const u32 stream_write_orignal[] = {
	0x7c0802a6,
	0x90010004,
	0x9421ffe0,
	0x93e1001c,
	0x3be50000,
	0x93c10018,
	0x3bc30000,
	0x38640000,
	0x38de0000,
	0x38800001
};

static const u32 stream_write_orignal2[] = {
	0x9421fff0,
	0x7c0802a6,
	0x90010014,
	0x93e1000c,
	0x7cbf2b78,
	0x93c10008,
	0x7c7e1b78,
	0x7c832378,
	0x38800001,
	0x7fc6f378
};

static const u32 stream_write_orignal3[] = {
	0x9421ffe0,
	0x7c0802a6,
	0x90010024,
	0x93e1001c,
	0x93c10018,
	0x7cbe2b78,
	0x90810008,
	0x38800001,
	0x93a10014,
	0x7c7d1b78,
	0x80610008,
	0x7fa6eb78
};
#endif

static const u32 dvd_seek_original[] = {
	0x7c0802a6,
	0x90010004,
	0x38000000,
	0x9421ffe8,
	0x93e10014,
	0x93c10010,
	0x908d9478,
	0x3c80cc00,
	0x38846000,
	0x900d9470,
	0x3c00ab00,
	0x90040008
};

static const u32 dvd_read_audio_original[] = {
	0x7c0802a6,
	0x90010004,
	0x38000000,
	0x9421ffe0,
	0x93e1001c,
	0x93c10018,
	0x90cdad68,
	0x3cc0cc00,
	0x38c66000,
	0x900dad60,
	0x6460e100
};

static const u32 dvd_audio_config_original[] = {
	0x7c0802a6,
	0x2c030000,
	0x90010004,
	0x38000000,
	0x9421ffe0,
	0x93e1001c,
	0x93c10018,
	0x90adad68,
	0x900dad60,
	0x41820008,
	0x3c000001,
	0x6400e400,
	0x3c60cc00,
	0x7c800378,
	0x38636000,
	0x90030008
};

static const u32 _dvdlowreadid_org[] = {
	0x7C0802A6,
	0x39000000,
	0x90010004,
	0x3CA0A800,
	0x38050040,
	0x9421FFE8,
	0x38C00020,
	0x3CA08000,
	0x93E10014,
	0x93C10010	
};

static const u32 _dvdlowreadid_org2[] = {
	0x7c0802a6,
	0x3ca0a800,
	0x90010004,
	0x38050040,
	0x38c00020,
	0x9421ffe8,
	0x3ca08000,
	0x93e10014,
	0x93c10010,
	0x908d16bc,
	0x3c80cc00,
	0x38e46000,
	0x90046008
};

#if defined(MIOS_PLUGIN) || defined(HIGH_PLUGIN) /* Only MIOS and high version has enough memory. */
static const u32 _dvdlowreadid_org3[] = {
	0x7c0802a6,
	0x90010004,
	0x9421ffe8,
	0x93e10014,
	0x7c7f1b78,
	0x9081000c,
	0x57e006ff,
	0x41820020,
	0x0,
	0x0,
	0x0,
	0x0,
	0x0,
	0x4cc63182,
	0x4be71191,
	0x8001000c,
	0x900da720,
	0x38000000,
	0x900da718,
	0x3c80a800,
	0x38840040,
	0x3c60cc00,
	0x90836008
};
#endif

#ifdef GEKKO_DEBUG
#if defined(MIOS_PLUGIN) || defined(HIGH_PLUGIN) /* Only MIOS and high version has enough memory. */
static const u32 dvd_stop_motor_original[] = {
	0x7c0802a6,
	0x90010004,
	0x38000000,
	0x9421ffe8,
	0x93e10014,
	0x93c10010,
	0x906d8988,
	0x3c60cc00,
	0x38836000,
	0x900d8980,
	0x3c00e300,
	0x90036008
};
#endif
#endif

static const u32 dvd_report_error_original[] = {
	0x7c0802a6,
	0x90010004,
	0x38000000,
	0x9421ffe8,
	0x93e10014,
	0x93c10010,
	0x906d8988,
	0x3c60cc00,
	0x38836000,
	0x900d8980,
	0x3c00e000,
	0x90036008
};


static const u32 dvd_audio_status_original[] = {
	0x7c0802a6,
	0x90010004,
	0x38000000,
	0x9421ffe8,
	0x93e10014,
	0x93c10010,
	0x908d8988,
	0x3c80cc00,
	0x38846000,
	0x900d8980,
	0x6460e200,
};

static const u32 memset_original[] = {
	0x28050020,
	0x5484063e,
	0x38c3ffff,
	0x7c872378,
	0x41800090,
	0x7cc030f8,
	0x540307bf,
	0x41820014,
	0x7ca32850,
	0x3463ffff,
	0x9ce60001,
	0x4082fff8
};

#if defined(RELOAD_SUPPORT) && defined(HIGH_PLUGIN) && defined(DONTTELL)
extern void patchhook(u32 address, u32 len);
#endif

/** Patch DVD read function. The patch should be applied only one time. */
void dvd_patchread(void *addr, u32 len, gcplugin_jumptable_t *jmp, int override) {
	void *addr_start = addr;
	void *addr_end = addr+len;

#ifdef RELOAD_SUPPORT
	// .dols should usually be loaded near 0x80003000, 8 MB max size estimated
	// The Bootloader/IPL is loaded to 0x81300000, 1 MB max size estimated

#ifdef MIOS_PLUGIN
	// The optimisation doesn't work on this
	if ( !override && (reloader_enabled != 1 || (u32)addr_end <= 0x80003000 || (u32)addr >= 0x81400000
		|| ((u32)addr >= 0x80800000 && (u32)addr_end <= 0x81300000) ) )
	{
		return;
	}
#else
#ifdef ACTION_REPLAY 
	if 	(	reloader_enabled != 1 ||
			(jmpNr == action_replay && ((u32)addr_end <= 0x81300000 || (u32)addr_start >= 0x81400000))	// Running game .dol, need to patch bootloader
		)
	{
		// jmpNr != action_replay ==> Running bootloader, need to patch game .dol, just try to patch everything
		return;
	}
#else
	if 	(	reloader_enabled != 1 ||
			(jmpNr == 0 && ((u32)addr_end <= 0x81300000 || (u32)addr_start >= 0x81400000))	// Running game .dol, need to patch bootloader
		)
	{
		// jmpNr == 1 ==> Running bootloader, need to patch game .dol, just try to patch everything
		return;
	}
#endif
#endif
#endif	// #ifdef RELOAD_SUPPORT


	while(addr_start<addr_end) {
#if defined(RELOAD_SUPPORT) && defined(HIGH_PLUGIN) && defined(DONTTELL)
	// Only patch hooks in game .dols
		if (jmpNr == 1 && hooks != 0	
		&&		*(unsigned long *)((unsigned long)addr_start     ) == 0x906402E4
		&&		*(unsigned long *)((unsigned long)addr_start +  4) == 0x38000000
		&&		*(unsigned long *)((unsigned long)addr_start +  8) == 0x900402E0
		&&		*(unsigned long *)((unsigned long)addr_start + 12) == 0x909E0004	)
		{
			patchhook((unsigned long)addr_start, len);
		}
#endif
		
		if (codecmp(addr_start,_Read_original,sizeof(_Read_original))==0)  {
			if (*(unsigned long*)(addr_start +0x5C) == 0x3c60a800) {
				debug_printf("Patching DVD read at 0x%x\n", (u32) addr_start);

				install_replacement((void*)addr_start, jmp->dvd_read_replacement, jmp->dvd_read_orig);

				// Change DVD command from 0xA8 to 0xD0:
				*(unsigned long*)(addr_start +0x5C) = 0x3C60D000;
				// Shift (>>11) the size and put in r3:
				*(unsigned long*)(addr_start +0x64) = 0x57A3AAFE;
				// Don't shift (>>2) the offset, because this is now LBA:
				*(unsigned long*)(addr_start +0x6C) = 0x93C4000C;
				// Store shifted size value:
				*(unsigned long*)(addr_start +0x74) = 0x90640010;
			}
		}

		if (codecmp(addr_start,_Read_original2,sizeof(_Read_original2))==0)  {
			debug_printf("Patching DVD read 2 at 0x%x\n", (u32) addr_start);

			install_replacement((void*)addr_start, jmp->dvd_read_replacement, jmp->dvd_read_orig);

			// Change DVD command from 0xA8 to 0xD0:
			*(unsigned long*)(addr_start +0x44) = 0x3C00D000;
			// Don't shift (>>2) the offset, because this is now LBA:
			// Shift (>>11) the size and put in r4:
			*(unsigned long*)(addr_start +0x54) = 0x57E4AAFE;
			// Store shifted size value:
			*(unsigned long*)(addr_start +0x64) = 0x90836010;
		}

		if (codecmp(addr_start,_Read_original3,sizeof(_Read_original3))==0)  {
			debug_printf("Patching DVD read 3 at 0x%x\n", (u32) addr_start);

			install_replacement((void*)addr_start, jmp->dvd_read_replacement, jmp->dvd_read_orig);

			// Change DVD command from 0xA8 to 0xD0:
			*(unsigned long*)(addr_start +0x54) = 0x3C60D000;
			// Shift (>>11) the size and put in r3:
			*(unsigned long*)(addr_start +0x5c) = 0x57A3AAFE;
			// Don't shift (>>2) the offset, because this is now LBA:
			*(unsigned long*)(addr_start +0x64) = 0x93C4000C;
			// Store shifted size value:
			*(unsigned long*)(addr_start +0x6c) = 0x90640010;
		}
		
		if ((codecmp(addr_start,_dvdlowreadid_org,sizeof(_dvdlowreadid_org))==0)
			|| (codecmp(addr_start,_dvdlowreadid_org2,sizeof(_dvdlowreadid_org2))==0)
#if defined(MIOS_PLUGIN) || defined(HIGH_PLUGIN) /* Only MIOS and high version has enough memory. */
			|| (codecmp(addr_start,_dvdlowreadid_org3,sizeof(_dvdlowreadid_org3))==0)
#endif
			) {
			debug_printf("Patching DVD Read ID at 0x%x\n", (u32) addr_start);

			install_replacement((void*)addr_start, jmp->dvd_read_id_replacement, jmp->dvd_read_id_orig);
		}

		// All plugins do this
		//if (jmp->dvd_report_error_replacement != 0)
		{
			if (codecmp(addr_start,dvd_report_error_original,sizeof(dvd_report_error_original))==0)  {
				debug_printf("Patching DVD report error at 0x%x\n", (u32) addr_start);
	
				install_replacement((void*)addr_start, (void *) jmp->dvd_report_error_replacement, (void *) jmp->dvd_report_error_orig);
			}
		}

		// All plugins do this
		//if (jmp->dvd_audio_status_replacement != 0)
		{
			if (codecmp(addr_start,dvd_audio_status_original,sizeof(dvd_audio_status_original))==0)  {
				debug_printf("Patching DVD audio status at 0x%x\n", (u32) addr_start);
	
				install_replacement((void*)addr_start, (void *) jmp->dvd_audio_status_replacement, (void *) jmp->dvd_audio_status_orig);
			}
		}

		// All plugins do this
		//if (jmp->dvd_read_audio_replacement != 0)
		{
			if (codecmp(addr_start,dvd_read_audio_original,sizeof(dvd_read_audio_original))==0) {
				debug_printf("Patching DVD read audio at 0x%x\n", (u32) addr_start);
	
				writebranch((void*) addr_start, (void *) jmp->dvd_read_audio_replacement);
			}
		}

		// All plugins do this
		//if (jmp->dvd_audio_config_replacement != 0)
		{
			if (codecmp(addr_start,dvd_audio_config_original,sizeof(dvd_audio_config_original))==0) {
				debug_printf("Patching DVD audio config at 0x%x\n", (u32) addr_start);
	
				writebranch((void*) addr_start, (void *) jmp->dvd_audio_config_replacement);
			}
		}
		
		// All plugins do this
		// if (jmp->memset_replacement != 0)
		{
			if (codecmp(addr_start, memset_original, sizeof(memset_original))==0)  {
				debug_printf("Patching memset at 0x%x\n", (u32) addr_start);
	
				install_replacement((void*)addr_start, (void *) jmp->memset_replacement, jmp->memset_orig);
			}
		}

		// All plugins do this
		//if (jmp->dvd_seek_replacement != 0)
		{
			if (codecmp(addr_start,dvd_seek_original,sizeof(dvd_seek_original))==0) {
				debug_printf("Patching DVD seek at 0x%x\n", (u32) addr_start);
	
				writebranch((void*) addr_start, (void *) jmp->dvd_seek_replacement);
			}
		}

#if defined(MIOS_PLUGIN) || defined(HIGH_PLUGIN) /* Only MIOS and high version has enough memory. */
#ifdef ACTION_REPLAY
		// TODO check if this is necessary at all, does the reloader load action replay?
		if (jmp->ar_dvd_read_replacement != 0)
		{
			if (codecmp(addr_start, ar_dvd_high_read, sizeof(ar_dvd_high_read))==0)  {
				debug_printf("Patching DVD ar read at 0x%x\n", (u32) addr_start);

				writeabsolutebranch((void*)addr_start, jmp->ar_dvd_read_replacement);

				/* Get address for dvd wait cmd. */
				jmp->ar_dvd_wait_for_last_cmd = (void *) get_bl_target((uint32_t *)(addr_start + 0x2c));
			}
		}

		// TODO check if this is necessary at all, does the reloader load action replay?
		if (jmp->ar_dvd_read_disk_id_replacement != 0)
		{
			if (codecmp(addr_start, ar_dvd_high_read_disk_id, sizeof(ar_dvd_high_read_disk_id))==0)  {
				debug_printf("Patching DVD ar read disk id at 0x%x\n", (u32) addr_start);

				writeabsolutebranch((void*)addr_start, jmp->ar_dvd_read_disk_id_replacement);

				/* Get address for dvd wait cmd. */
				jmp->ar_dvd_wait_for_last_cmd = (void *) get_bl_target((uint32_t *)(addr_start + 0x1c));
			}
		}
#endif
#endif

#ifdef GEKKO_DEBUG
		/* Patch debug printf. */
		if (jmp->stream_write_replacement != 0) {
			if ((codecmp(addr_start, stream_write_orignal, sizeof(stream_write_orignal))==0)
				|| (codecmp(addr_start, stream_write_orignal2, sizeof(stream_write_orignal2))==0)
				|| (codecmp(addr_start, stream_write_orignal3, sizeof(stream_write_orignal3))==0)) {
				debug_printf("Patching debug write at 0x%x\n", (u32) addr_start);

				writebranch((void*)addr_start, (void *) jmp->stream_write_replacement);
			}
		}
#if defined(MIOS_PLUGIN) || defined(HIGH_PLUGIN) /* Only MIOS and high version has enough memory. */
		if (jmp->dvd_stop_motor_replacement != 0) {
			if (codecmp(addr_start,dvd_stop_motor_original,sizeof(dvd_stop_motor_original))==0)  {
				debug_printf("Patching DVD motor stop at 0x%x\n", (u32) addr_start);
	
				install_replacement((void*)addr_start, (void *) jmp->dvd_stop_motor_replacement, (void *) jmp->dvd_stop_motor_orig);
			}
		}
#endif
#endif
#if 0 // XXX: Action Replay v1.06 vsprintf patch
#ifdef ACTION_REPLAY
#ifdef MIOS_PLUGIN /* Action replay is only loaded by MIOS. */
		if (addr_start == (void *) 0x8002788c) { /* vsprintf, AR v1.06. */
			if (*((uint32_t *) addr_start) == 0x9421ffe0) {
				if (jmp->ar_stream_write_replacement != 0) {
					debug_printf("patching debug write at 0x%x (ar only)\n", (u32) addr_start);

					install_replacement((void*)addr_start, jmp->ar_stream_write_replacement, jmp->ar_stream_write_orig);
				}
			}
		}
#endif
#endif
#endif
		addr_start += 4;
	}
}
