/*
 *
 * 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., 59 Temple Place, Suite 330, Boston,
 * MA 02111-1307 USA
 */
#include <stdio.h>
#include <unistd.h>
#include <ogcsys.h>
#include <gccore.h>
#include <ogc/cache.h>
#include <string.h>
#include <stdint.h>

#include "dvd.h"
#include "dvdreadpatch.h"
#include "highplugin_bin.h"
#include "lowplugin_bin.h"
#include "gcplugin.h"
#include "debugprintf.h"
#include "loadmios.h"
#include "loadapp.h"
#include "systemconfig.h"
#include "patchcode.h"
#include "asm.h"
#include "processor.h"
#include "gameloader.h"

#include "Toy.h"

#include "GCcodehandler_bin.h"

/** Time difference between UNIX time and GC time. */
#define GC_TIME_OFFSET 946684800
/** GameCube bus clock. */
#define GC_BUS_CLOCK 0x09A7EC80ULL
#define GC_BOOT_TIME ((unsigned long long *) 0x800030D8)

extern void __exception_closeall();

typedef struct {
	const char *gameid;
	u32 address1; /* Start address for patch search of second (fourth,...) DOL. */
	u32 size1; /* Size of patch. */
	u32 number1; /* Number of DOL changes. */
	u32 address2; /* Start address for patch search of first (third,...) DOL (Game Loader). */
	u32 size2; /* Size of Patch. */
	u32 number2; /* Number of DOL changes. */
} patch_entry_t;

/** Get timebase register, exported by libogc. */
extern long long gettime(void);

extern u32 hookselect;
extern u32 hook2select;
extern u32 ocarinaselect;
extern u32 debuggerselect;
/*
patch_entry_t game_patches[] = {
	{
		// Zelda Collection.
		.gameid = "PZLJ",

		// Patch for Game Loader.
		.address1 = 0x81307ebc,
		.size1 = 0x100,
		.number1 = 1,

		// Patch for final Game.
		.address2 = 0x8030a4ac,
		.size2 = 0x100,
		.number2 = 1,
	}
};*/

void nothing()
{
}

void setup_time(void)
{
	time_t currentTime;
	unsigned long long curTime;
	struct tm *gm;
	unsigned long long timebase;
	volatile unsigned long long *gcstarttime = GC_BOOT_TIME;

	time(&currentTime);
	debug_printf("time 0x%08x\n", (unsigned long) currentTime);
	gm = gmtime(&currentTime);
	debug_printf("time: %s\n", asctime(gm));

	/* Convert to GameCube time format. */
	curTime = currentTime;
	curTime -= GC_TIME_OFFSET;
	curTime = curTime * GC_BUS_CLOCK;
	timebase = gettime();
	timebase = curTime - timebase;

	*gcstarttime = timebase;
	DCFlushRange((void *) gcstarttime, 4);
	debug_printf("time 0x%08x%08x\n", (unsigned long) (*gcstarttime >> 32), (unsigned long) *gcstarttime);
}



extern bool patched_read;
extern bool patched_readMIOS;
extern bool patched_seek;
extern bool patched_readID;
extern bool patched_readIDMIOS;
extern bool patched_readaudio;
extern bool patched_audioconfig;
extern bool patched_audiostatus;
extern bool patched_reporterror;



//---------------------------------------------------------------------------------
void load_handler()
//---------------------------------------------------------------------------------
{
	memcpy((void*)0x80001800,GCcodehandler_bin,GCcodehandler_bin_size);
	memcpy((void *)0x80001800, (void*)0x80000000, 6);
	
	// The codes are copied earlier
	
	DCFlushRange((void*)0x80001800, 0x1800);
	ICInvalidateRange((void*)0x80001800, 0x1800);
}

void load_app(u8 reloader)
{
	gcplugin_jumptable_t *jmp1 = NULL;
	gcplugin_jumptable_t *jmp2 = NULL;
	u32 plugin_size = 0;
	const u8 *plugin = NULL;
	void *plugin_base = 0;
	int rv;

	void (*app_init)(void (*report)(const char* fmt, ...));
	int (*app_main)(void** dst, int* size, int* offset);
	void* (*app_final)();

	void (*app_entry)(void(**init)(void (*report)(const char* fmt, ...)),
		int (**main)(), void *(**final)());

	static char apploader_header[32] __attribute__((aligned(32)));

	if (backuploading)
	{
		printf("Loading backup disc...\n");
	} else
	{
		printf("Loading retail disc...\n");
	}

	setup_time();

	// Read the disc id
	rv = dvd_read((void *) GAMECODE_ADDRESS, 32, 0);
	DVD_CHECK();
	
	// Read the name of the game
	static dvdinfo_t gcm_disk __attribute((aligned(32)));
	rv = dvd_read(&gcm_disk, sizeof(gcm_disk), 0);
	DVD_CHECK();

	char discid[7];
	memset(discid, 0, 7);
	memcpy(discid, (void *) GAMECODE_ADDRESS, 6);
	
	printf("DiscID: %s, Name: %s\n", discid, gcm_disk.GameName);

	rv = dvd_read(apploader_header, sizeof(apploader_header), APPLOADER_HEADER_OFFSET);
	DVD_CHECK();

	debug_printf("Reading apploader address 0x%x, len 0%x, offset 0x%x\n",
		(void*)APPLOADER_ADDRESS,
		((*(unsigned long*)(apploader_header + 0x14)) + 31) & ~31,
		APPLOADER_OFFSET);
	rv = dvd_read((void*)APPLOADER_ADDRESS,
		((*(unsigned long*)(apploader_header + 0x14)) + 31) & ~31,
		APPLOADER_OFFSET);
	DVD_CHECK();

	app_entry = (void (*)(void(**)(void (*)(const char*, ...)), int (**)(), void *(**)()))(*(unsigned long*)(apploader_header + 0x10));
	app_entry(&app_init, &app_main, &app_final);
	app_init((void (*)(const char*, ...))nothing);

	// According to emu_kidid Pokemon Box: Ruby and Sapphire writes some data to 0x80001800 and 0x80002600, and reads it. The game is confirmed to not work with the low plugin.
	if (	(memcmp((void *) GAMECODE_ADDRESS, "GPX", 3) == 0
		&& 	memcmp((void *)((u32)GAMECODE_ADDRESS+4),"01" , 2) == 0 )
#ifdef DONTTELL
		|| ( backuploading && (hookselect != 0 || hook2select != 0) && (ocarinaselect != 0 || debuggerselect != 0) )
#endif
	// Action Replay
		|| 	memcmp((void *) GAMECODE_ADDRESS, "GNHE", 4) == 0)
	{
		plugin_size = highplugin_bin_size;
		plugin = highplugin_bin;
		plugin_base = HIGH_PLUGIN_BASE;
		setup_sys_config(-1);
		printf("Using high plugin...\n");
	} else {
		plugin_size = lowplugin_bin_size;
		plugin = lowplugin_bin;
		plugin_base = LOW_PLUGIN_BASE;
		setup_sys_config(0);
	}
	jmp1 = (gcplugin_jumptable_t *) plugin_base;
	jmp2 = (gcplugin_jumptable_t *) (((u32) plugin_base) + sizeof(gcplugin_jumptable_t));
	
	//debug_printf("magic 0x%08x\n", jmp1->magic);

	if (backuploading)
	{
		/* Install DVD replacement functions. */
		memcpy(plugin_base, plugin, plugin_size);

		/* Check for Reloader. */
		//if (jmp1->magic == RELOAD_MAGIC) {
#ifdef DONTTELL
			*(u32 *) (((u32) plugin_base) + 2*sizeof(gcplugin_jumptable_t) + 32) = base_offset;
			*(u32 *) (((u32) plugin_base) + 2*sizeof(gcplugin_jumptable_t) + 32 + sizeof(u32)) = second_disc_offset;
			if (reloader)
			{
				*(u32 *) (((u32) plugin_base) + 2*sizeof(gcplugin_jumptable_t) + 32 + 2*sizeof(u32)) = 1;			
				printf("Reloader activated...\n");
			} else
			{
				*(u32 *) (((u32) plugin_base) + 2*sizeof(gcplugin_jumptable_t) + 32 + 2*sizeof(u32)) = 0;	
			}			

			if ((hookselect != 0 || hook2select != 0) && (ocarinaselect != 0 || debuggerselect != 0))
			{
				*(u32 *) (((u32) plugin_base) + 2*sizeof(gcplugin_jumptable_t) + 32 + 3*sizeof(u32)) = 1;	
			} else
			{
				*(u32 *) (((u32) plugin_base) + 2*sizeof(gcplugin_jumptable_t) + 32 + 3*sizeof(u32)) = 0;	
			}
			copy_2nd_hook((void *)(((u32) plugin_base) + 2*sizeof(gcplugin_jumptable_t)));
#else
			*(u32 *) (((u32) plugin_base) + 2*sizeof(gcplugin_jumptable_t)) = base_offset;
			*(u32 *) (((u32) plugin_base) + 2*sizeof(gcplugin_jumptable_t) + sizeof(u32)) = second_disc_offset;
			if (reloader)
			{
				*(u32 *) (((u32) plugin_base) + 2*sizeof(gcplugin_jumptable_t) + 2*sizeof(u32)) = 1;			
				printf("Reloader activated...\n");
			} else
			{
				*(u32 *) (((u32) plugin_base) + 2*sizeof(gcplugin_jumptable_t) + 2*sizeof(u32)) = 0;	
			}			
#endif
			debug_printf("Reloader is supported.\n");
		//} else {
		//	*(u32 *) (((u32) plugin_base) + 1*sizeof(gcplugin_jumptable_t)) = base_offset;
		//	*(u32 *) (((u32) plugin_base) + 1*sizeof(gcplugin_jumptable_t) + sizeof(u32)) = second_disc_offset;
		//	debug_printf("Reloader is not supported.\n");
		//}
	}

	if (backuploading)
	{
		DCFlushRange(plugin_base, plugin_size);
		ICInvalidateRange(plugin_base, plugin_size);
	}

	toy_reset();

	bool hookpatched = false;
	while (1)
	{
		void* dst = 0;
		int len = 0,offset = 0;
		int res = app_main(&dst, &len, &offset);

		if (!res)
			break;
		debug_printf("Reading section buffer 0x%x, len 0%x, offset 0x%x\n", dst, len, offset);
		rv = dvd_read(dst, len, offset);
		DVD_CHECK();
		
		dvd_patchread(dst, len, (gcplugin_jumptable_t *)plugin_base, 0, (char *) GAMECODE_ADDRESS, backuploading);

#ifdef DONTTELL
		if ((hookselect != 0) && (ocarinaselect != 0 || debuggerselect != 0))
#else
		if (!backuploading && hookselect != 0 && (ocarinaselect != 0 || debuggerselect != 0))
#endif
		{
			if (dogamehooks(dst,len))
			{
				hookpatched = true;
			}
		}
		toy_register_apploader_file_loading(dst, len);

		DCFlushRange(dst, len);
		ICInvalidateRange(dst, len);
	}
	
	if (backuploading)
	{
		DCFlushRange(plugin_base, plugin_size);
		ICInvalidateRange(plugin_base, plugin_size);
	}
	
	char text[32];
	if (backuploading)
	{
		snprintf(text, 32, "WARNING: Could");
	} else
	{
		snprintf(text, 32, "WARNING: Would");
	}
	
	if (!patched_read || !patched_seek || !patched_readID || !patched_readaudio || !patched_audioconfig || !patched_audiostatus || !patched_reporterror)
	{
		printf("\n");
		if (!patched_read)
		{
			if (patched_readMIOS)
			{
				printf("%s patch read, but the reloader wouldn't\n", text);
			} else
			{
				printf("%s not patch read\n", text);
			}
		}
		
		if (!patched_readID)
		{
			if (patched_readIDMIOS)
			{
				printf("%s patch read id, but the reloader wouldn't\n", text);
			} else
			{
				printf("%s not patch read id\n", text);
			}
		}
		
		if (!patched_seek)
		{
			printf("%s not patch seek\n", text);
		}

		if (!patched_readaudio)
		{
			printf("%s not patch read audio\n", text);
		}

		if (!patched_audioconfig)
		{
			printf("%s not patch audio confign", text);
		}

		if (!patched_audiostatus)
		{
			printf("%s not patch audio status\n", text);
		}

		if (!patched_reporterror)
		{
			printf("%s not patch report error\n", text);
		}
		printf("\n");

		sleep(15);
	}
	
#ifdef DONTTELL
	if ((hookselect != 0) && (ocarinaselect != 0 || debuggerselect != 0))
#else
	if (!backuploading && hookselect != 0 && (ocarinaselect != 0 || debuggerselect != 0))
#endif
	{
		if (!hookpatched)
		{
			printf("Hook not patched\n");
			sleep(5);
		} else
		{
			printf("Hook patched\n");
			sleep(2);
		}
		load_handler();
	}

	debug_printf("Starting game by backuplauncher.\n");

	toy_patch_video_modes();

	if (dvd_set_streaming())
	{
		printf("Enabled audio streaming\n");
	}

	sleep(5);
	
	setvideomode();
	
	/* Close Exceptions: */
	unsigned long level = 0;
	_CPU_ISR_Disable(level);
	__exception_closeall();

#ifdef DONTTELL
	if ((hookselect != 0 || hook2select != 0) && (ocarinaselect != 0 || debuggerselect != 0))
#else
	if (!backuploading && hookselect != 0 && (ocarinaselect != 0 || debuggerselect != 0))
#endif
	{		
		appentrypoint = (u32)app_final();
		__asm__(
			"lis %r3, appentrypoint@h\n"
			"ori %r3, %r3, appentrypoint@l\n"
			"lwz %r3, 0(%r3)\n"
			"mtlr %r3\n"
			"lis %r3, 0x8000\n"
			"ori %r3, %r3, 0x18A8\n"
			"mtctr %r3\n"
			"bctr\n"
		);		
	} else
	{	
		void (*entrypoint)() = app_final();
		entrypoint();
	}	
}

