#include <stdio.h>
#include <stdlib.h>
#include <ogc/cache.h>
#include <ogc/system.h>
#include <ogcsys.h>
#include <gccore.h>

#include <string.h>
#include <unistd.h>

#include "loadapp.h"
#include "dvdreadpatch.h"
#include "miosplugin_bin.h"
#include "lowplugin_bin.h"
#include "gcplugin.h"
#include "loadmios.h"
#include "debugprintf.h"
#include "dvd.h"
#include "writebranch.h"

/** Start address of MIOS v5. */
#define MIOS_BASE ((void *) 0x812fffe0)
/** Size of MIOS v5. */
#define MIOS_SIZE 0x0002659c
/** Entry point for MIOS v5. */
#define MIOS_ENTRY 0x81300200
/** Entry point variable. */
#define MIOS_PATCH_ADDR ((void *) 0x800037fcUL)
/** Pointer to address which set the memory end address. This is used for clearing memory. */
#define MIOS_V5_MEM_END_INSTR 0x813007ecUL
/** Entry point for GC HomeBrew Launcher code (patched into MIOS). */
#define GC_HOMEBREW_LAUNCHER_ENTRY 0x81000000UL
/** Branch with link instruction. */
#define INSN_BL 0x48000001UL
/** Mask for branch with link. */
#define MASK_BL 0xFC000001UL
/** Assembler instruction "lis %r5, 0x0130". */
#define INSN_LIS_R5_0130 0x3CA00130UL
/** Complete instruction compare. */
#define MASK_ALL 0xFFFFFFFFUL
/** Clear memory at this address. */
#define CLEAR_MEM_ADDR 0x81340000
/** Size of memroy to clear. */
#define CLEAR_MEM_SIZE 0x4c0000

syssram* __SYS_LockSram(void);
u32 __SYS_UnlockSram(u32 write);
u32 __SYS_SyncSram(void);

void hexDump(unsigned char *buffer, unsigned long size)
{
	int i;

	for (i = 0; i < size; i++) {
		if ((i > 0) && ((i & 0x3) == 0)) {
			debug_printf("\n");
		}
		if ((i & 0x3) == 0) {
			debug_printf("%x: ", i);
		}
		unsigned char l;

		l = buffer[i];
		debug_printf("%02x", l);
		if ((i + 1) < size) {
			debug_printf(" ");
		}
	}
	debug_printf("\n");
}

/** Find next assembler instruction matching mask and insn. */
uint32_t *find_insn(uint32_t *start, uint32_t mask, uint32_t insn)
{
	while(((uint32_t) start) < ((uint32_t) (MIOS_BASE + MIOS_SIZE))) {
		if ((*start & mask) == insn) {
			return start;
		}
		start++;
	}
	debug_printf("Error: INSN 0x%08x not found.\n", insn);
	return NULL;
}

/** Find next branch with link inctruction. */
uint32_t *find_bl(uint32_t *start)
{
	return find_insn(start, MASK_BL, INSN_BL);
}

/** Find instruction wich sets r5 to 0x01300000. */
uint32_t *find_lis_r5_0130(uint32_t *start)
{
	return find_insn(start, MASK_ALL, INSN_LIS_R5_0130);
}

/** Search for "count" occurance of branch with link. */
uint32_t *find_for_bl(uint32_t *pc, uint32_t count)
{
	int i;

	for (i = 0; i < count; i++) {
		pc = find_bl(pc);
		if (pc == NULL) {
			return NULL;
		}
		pc++;
	}
	pc--;

	return pc;
}

/** Get address for MIOS clear memory size patch. */
uint32_t *get_size_patch_addr(void)
{
	uint32_t *pc;

	pc = (uint32_t *) MIOS_ENTRY;

	pc = find_for_bl(pc, 6);
	if (pc == NULL) {
		return NULL;
	}

	pc = get_bl_target(pc);

	pc = find_for_bl(pc, 2);
	if (pc == NULL) {
		return NULL;
	}

	pc = get_bl_target(pc);

	return find_lis_r5_0130(pc);
}

/** Start PowerPC MIOS part and patch some code before. */
void call_mios(void)
{
	unsigned long *entry = MIOS_PATCH_ADDR;

	memset((void *) CLEAR_MEM_ADDR, 0, CLEAR_MEM_SIZE);
	DCFlushRange((void *) CLEAR_MEM_ADDR, CLEAR_MEM_SIZE);
	ICInvalidateRange((void *) CLEAR_MEM_ADDR, CLEAR_MEM_SIZE);

	if (*entry != GC_HOMEBREW_LAUNCHER_ENTRY) {
		printf("MIOS is overwritten. Please reboot (0x%lx).\n", *entry);
		while(1);
	}

	/* Restore patched entry point. */
	*entry = MIOS_ENTRY;
	DCFlushRange(entry, 32);

	/* Simulate boot. */
	__asm__(
		"bl DCDisable\n"
		"bl ICDisable\n"
		"li %r3, 0\n"
		"mtsrr1 %r3\n"
		"li %r4, 0x3400\n"
		"mtsrr0 %r4\n"
		"rfi\n"
	);
}

/** Set end address for memory clear in MIOS. */
int setMemoryEndAddress(void *endAddr)
{
	volatile unsigned long *memPatch = (unsigned long *) get_size_patch_addr();
	unsigned long memEnd;

	if (memPatch == NULL) {
		printf("   Size patch failed.\n");
		sleep(3);
		return 0;
	}

	memEnd = (unsigned long) endAddr;

	/* Convert to physical address. */
	memEnd = memEnd & 0x0FFFFFFF;

	/* Initialise lower memory only. */
	debug_printf("Old instruction 0x%08x at 0x%08x\n", memPatch, memPatch[0]);
	memPatch[0] = (memPatch[0] & 0xFFFF0000) | (memEnd >> 16);
	debug_printf("New instruction 0x%08x\n", memPatch[0]);

	DCFlushRange((void *) memPatch, 32);
	ICInvalidateRange((void *) memPatch, 32);
	return -1;
}

void set_video_mode(int mode)
{
	syssram *sram;
	int miosmode;

	/* Select MIOS video mode by disc mode. */
	switch (mode) {
	case 'E':
	case 'J':
		/* NTSC */
		miosmode = 0;
		break;

	case 'D':
	case 'P':
	case 'F':
	case 'X':
	case 'Y':
	case 'U':
		/* PAL */
		miosmode = 1;
		break;
	default: {
			/* Unknown mode. Don't change. */
			int tv;

			tv = VIDEO_GetCurrentTvMode();
			switch(tv) {
				case VI_NTSC:
					/* NTSC */
					miosmode = 0;
					break;

				case VI_MPAL:
				case VI_PAL:
				case VI_EURGB60:
					/* PAL */
					miosmode = 1;
					break;
				default:
					/* NTSC */
					miosmode = 0;
					break;
			}
			break;
		}
	}

	debug_printf("Setting MIOS video mode to %s.\n", (miosmode == 1) ? "PAL" : "NTSC");
	sram = __SYS_LockSram();
	/* Set PAL mode. */
	sram->flags = (sram->flags & ~0x03) | miosmode;
	__SYS_UnlockSram(TRUE);
	while(!__SYS_SyncSram());
}

#if 0
void setHeapEnd(u32 vaddr)
{
	u32 paddr = vaddr & 0x1FFFFFFF;
	volatile u32 *patch;

	patch = (u32 *) 0x81300814; // XXX: address only MIOS v5
	*patch = (*patch & 0xFFFF0000) | (paddr >> 16);
	DCFlushRange(patch, 4);
	ICInvalidateRange(patch, 4);
	
	patch = (u32 *) 0x81300824; // XXX: address only MIOS v5
	*patch = (*patch & 0xFFFF0000) | (vaddr >> 16);
	DCFlushRange(patch, 4);
	ICInvalidateRange(patch, 4);
}
#endif

void load_mios(void)
{
	/* Start game using MIOS. */
	void *dst = MIOS_BASE;
	unsigned long len = MIOS_SIZE;
	int doPatch = -1;
	static char gamecode[32] __attribute__((aligned(32)));
	int rv;

	rv = dvd_read(gamecode, sizeof(gamecode), 0);
	DVD_CHECK();

	memcpy((void *) GAMECODE_ADDRESS, gamecode,  sizeof(gamecode));
	DCFlushRange((void *) GAMECODE_ADDRESS, sizeof(gamecode));

	/* Set MIOS video mode. */
	set_video_mode(gamecode[3]);

	if (((unsigned long) MIOS_PLUGIN_BASE) >= 0x80003100UL) {
		/* Set end address for memory clear, so that MIOS plugin is not overwritten. */
		doPatch = setMemoryEndAddress(MIOS_PLUGIN_BASE);

		/* Install DVD replacement functions for games. */
		memcpy(LOW_PLUGIN_BASE, lowplugin_bin, lowplugin_bin_size);

		DCFlushRange(LOW_PLUGIN_BASE, lowplugin_bin_size);
		ICInvalidateRange(LOW_PLUGIN_BASE, lowplugin_bin_size);

		// XXX: setHeapEnd(HIGH_PLUGIN_BASE);
	}

	/* Patch only if this would have success. */
	if (doPatch) {
		gcplugin_jumptable_t *jmp = (gcplugin_jumptable_t *) MIOS_PLUGIN_BASE;

		/* Install DVD replacement functions. */
		memcpy(MIOS_PLUGIN_BASE, miosplugin_bin, miosplugin_bin_size);
		//debug_printf("magic 0x%08x\n", jmp->magic);

		//if (jmp->magic == RELOAD_MAGIC) {
			*(u32 *) (((u32) MIOS_PLUGIN_BASE) + 2*sizeof(gcplugin_jumptable_t)) = base_offset;
			*(u32 *) (((u32) MIOS_PLUGIN_BASE) + 2*sizeof(gcplugin_jumptable_t) + sizeof(u32)) = second_disc_offset;

			/* Reloader supported. */
			debug_printf("Reloader Supported.\n");
	
			/* First table is used for MIOS. */
	
			/* Second table is used for games. */
			jmp = (gcplugin_jumptable_t *) (((u32) MIOS_PLUGIN_BASE) + sizeof(gcplugin_jumptable_t));
	
			/* Enable patching. */
			//jmp->reload_patch_start = 0;
			//jmp->reload_patch_size = 0;
			//jmp->reload_patch_number = 1;
		//} else
		//{
		//	*(u32 *) (((u32) MIOS_PLUGIN_BASE) + 1*sizeof(gcplugin_jumptable_t)) = base_offset;
		//	*(u32 *) (((u32) MIOS_PLUGIN_BASE) + 1*sizeof(gcplugin_jumptable_t) + sizeof(u32)) = second_disc_offset;
		//}

		/* Patch MIOS DVD functions to use MIOS plugin. */
		//dvd_patchreset(dst, len, MIOS_PLUGIN_BASE);
		//dvd_patchreadid(dst, len, MIOS_PLUGIN_BASE);
		dvd_patchread(dst, len, MIOS_PLUGIN_BASE, 1, NULL, backuploading);
		DCFlushRange(dst, len);
		ICInvalidateRange(dst, len);

		DCFlushRange(MIOS_PLUGIN_BASE, miosplugin_bin_size);
		ICInvalidateRange(MIOS_PLUGIN_BASE, miosplugin_bin_size);
	}

	/* Jump to MIOS entry. */
	call_mios();
}
