#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#include <string.h>
#include <osd_config.h>

#include <kernel.h>
#include <fileio.h>
#include <sifrpc.h>

#include <loadfile.h>

#include "cnf_lite.h"

/* Some macros used for patching. */
#define JAL(addr)	(0x0c000000|(0x3ffffff&((addr)>>2)))
#define JMP(addr)	(0x08000000|(0x3ffffff&((addr)>>2)))
#define GETJADDR(addr)	((addr&0x03FFFFFF)<<2)

static char ver[64], cdboot_path[256], romver[16];
static unsigned char SelectedVMode;
static char ConsoleRegion=-1;

enum CONSOLE_REGIONS{
	CONSOLE_REGION_INVALID	= -1,
	CONSOLE_REGION_JAPAN	= 0,
	CONSOLE_REGION_USA,	//USA and HK/SG.
	CONSOLE_REGION_EUROPE,
	CONSOLE_REGION_CHINA,

	CONSOLE_REGION_COUNT
};

static unsigned int *ScanForPattern(unsigned int *start, unsigned int range, const unsigned int *pattern, unsigned int PatternSize, const unsigned int *mask){
	unsigned int i, *result, PatternOffset;

	for(result=NULL,i=0; i<range; i+=4){
		for(PatternOffset=0; PatternOffset<PatternSize; PatternOffset+=4){
			if((start[(i+PatternOffset)/4]&mask[PatternOffset/4])!=pattern[PatternOffset/4]) break;
		}

		if(PatternOffset==PatternSize){	//Pattern found.
			result=&start[i/4];
			break;
		}
	}

	return result;
}

static unsigned long int *emu_SYNCHV_I=(unsigned long int*)0x70001920;
static unsigned long int *emu_SYNCHV_NI=(unsigned long int*)0x70001928;

static int InitVideoModeParams(void){	//Replacement function. Overrides the SYCHV parameters set by the driver.
	unsigned long int temp;

	temp=0;	//It wasn't initialized in the Sony originals, but I hated the compiler warning.
	if(SelectedVMode==3){	//PAL
		*emu_SYNCHV_I=0x00A9000502101401;
		*emu_SYNCHV_NI=0x00A9000502101404;
	}
	else{	//NTSC
		temp=(temp&0xFFFFFC00)|3;
		temp=(temp&0xFFF003FF)|0x1800;
		temp=(temp&0xC00FFFFF)|0x01200000;
		temp=(temp&0xFFFFFC00FFFFFFFF)|(0xC000L<<19);
		temp=(temp&0xFFE003FFFFFFFFFF)|(0xF300L<<35);
		temp=(temp&0x801FFFFFFFFFFFFF)|(0xC000L<<40);
		*emu_SYNCHV_I=temp;

		//Nearly a repeat of the block above.
		temp=(temp&0xFFFFFC00)|4;
		temp=(temp&0xFFF003FF)|0x1800;
		temp=(temp&0xC00FFFFF)|0x01200000;
		temp=(temp&0xFFFFFC00FFFFFFFF)|(0xC000L<<19);
		temp=(temp&0xFFE003FFFFFFFFFF)|(0xF300L<<35);
		temp=(temp&0x801FFFFFFFFFFFFF)|(0xC000L<<40);
		*emu_SYNCHV_NI=temp;
	}

	return 0;
}

static void SetPS1DrvVideoMode(void *start, int region, int VideoMode){
	unsigned int *PatchLoc, *RetAddr;
	//For NTSC consoles.
	const static unsigned int VModeSetPatternNTSC[]={
		0x24040000,	//addiu a0, zero, VideoMode
		0x00661825,	//or v1, v1, a2
		0xaf840000,	//sw a0, VideoModeParam(gp)
	};
	const static unsigned int VModeSetPatternMaskNTSC[]={
		0xFFFF0000,
		0xFFFFFFFF,
		0xFFFF0000
	};
	//For PAL consoles.
	const static unsigned int VModeSetPatternPAL[]={
		0x24030000,	//addiu v1, zero, VideoMode
		0x3c0400a9,	//lui a0, $00a9
		0x34840005,	//ori a0, a0, $0005
	};
	const static unsigned int VModeSetPatternMaskPAL[]={
		0xFFFF0000,
		0xFFFFFFFF,
		0xFFFFFFFF
	};
	const static unsigned int VModeSetRetPattern[]={
		0x03e00008	//jr ra
	};
	const static unsigned int VModeSetRetPatternMask[]={
		0xFFFFFFFF
	};
	const unsigned int *pattern, *mask;
	unsigned int PatternSize;

	printf("SetPS1DrvVideoMode: Region: %d, video mode: %d.\n", region, VideoMode);

	if(region==CONSOLE_REGION_EUROPE){
		pattern=VModeSetPatternPAL;
		mask=VModeSetPatternMaskPAL;
		PatternSize=sizeof(VModeSetPatternPAL);
	}
	else{
		pattern=VModeSetPatternNTSC;
		mask=VModeSetPatternMaskNTSC;
		PatternSize=sizeof(VModeSetPatternNTSC);
	}

	if((PatchLoc=ScanForPattern(start, 0x20000, pattern, PatternSize, mask))!=NULL && (RetAddr=ScanForPattern(PatchLoc, 0x80, VModeSetRetPattern, sizeof(VModeSetRetPattern), VModeSetRetPatternMask))!=NULL){
		*PatchLoc=((*PatchLoc)&0xFFFF0000)|(VideoMode&3);
		*RetAddr=JMP((unsigned int)&InitVideoModeParams);
	}
	else printf("SetPS1DrvVideoMode: error - pattern not found.\n");
}

static void ChangePS1DrvVideoMode(void){
	SetPS1DrvVideoMode((void*)0x00200000, ConsoleRegion, SelectedVMode);

	FlushCache(0);
	FlushCache(2);
}

static void SetROMVERRegion(void *start, char region){
	unsigned int *PatchLoc;
	const static unsigned int ROMVERParserPattern[]={
		0x93a20004,	//lbu v0, $0004(sp)
		0x24030043,	//addiu v1, zero, $0043
		0x00021600,	//sll v0, v0, 24
		0x00022603	//sra v0, v0, 24
	};
	const static unsigned int ROMVERParserMask[]={
		0xFFFFFFFF,
		0xFFFFFFFF,
		0xFFFFFFFF,
		0xFFFFFFFF
	};

	printf("SetROMVERRegion: Region: %c\n", region);

	if((PatchLoc=ScanForPattern(start, 0x20000, ROMVERParserPattern, sizeof(ROMVERParserPattern), ROMVERParserMask))!=NULL){
		*PatchLoc=0x24020000|region;	//li $v0, region		
	}
	else printf("SetROMVERRegion: error - pattern not found.\n");
}

static void ChangeROMVERRegion(void){
	SetROMVERRegion((void*)0x00200000, SelectedVMode==3?'E':'A');

	FlushCache(0);
	FlushCache(2);
}

static int HookUnpackFunction(void *start){
	unsigned int *PatchLoc;
	int result;
	const static unsigned int UnpackFuncPattern[]={	//FlushCache(0); FlushCache(2);
		0x24030064,	//addiu v1, zero, $0064
		0x0000202d,	//daddu a0, zero, zero
		0x0000000c,	//syscall (00000)
		0x24030064,	//addiu v1, zero, $0064
		0x24040002,	//addiu a0, zero, $0002
		0x0000000c	//syscall (00000)
	};
	const static unsigned int UnpackFuncPatternMask[]={
		0xFFFFFFFF,
		0xFFFFFFFF,
		0xFFFFFFFF,
		0xFFFFFFFF,
		0xFFFFFFFF,
		0xFFFFFFFF
	};

	if((PatchLoc=ScanForPattern(start, 0x1000, UnpackFuncPattern, sizeof(UnpackFuncPattern), UnpackFuncPatternMask))!=NULL){
		PatchLoc[0]=0x3c020000|((unsigned int)&ChangeROMVERRegion)>>16;		//lui $v0, &ChangeROMVERRegion
		PatchLoc[1]=0x34420000|(((unsigned int)&ChangeROMVERRegion)&0xFFFF);	//ori $v0, &ChangeROMVERRegion
		PatchLoc[2]=0x0040f809;							//jalr $v0
		PatchLoc[3]=0;								//nop
		PatchLoc[4]=0;								//nop
		PatchLoc[5]=0;								//nop
		result=0;

		FlushCache(0);
		FlushCache(2);
	}
	else{
		printf("HookUnpackFunction: pattern not found. Not hooking.\n");
		result=1;
	}

	return result;
}

static unsigned short int GetBootROMVersion(void){
	int fd;
	char VerStr[5];

	fd=open("rom0:ROMVER", O_RDONLY);
	read(fd, romver, sizeof(romver));
	close(fd);
	VerStr[0]=romver[0];
	VerStr[1]=romver[1];
	VerStr[2]=romver[2];
	VerStr[3]=romver[3];
	VerStr[4]='\0';

	return((unsigned short int)strtoul(VerStr, NULL, 16));
}

static int GetConsoleRegion(void){
	int result;

	if((result=ConsoleRegion)<0){
		switch(romver[4]){
			case 'C':
				ConsoleRegion=CONSOLE_REGION_CHINA;
				break;
			case 'E':
				ConsoleRegion=CONSOLE_REGION_EUROPE;
				break;
			case 'H':
			case 'A':
				ConsoleRegion=CONSOLE_REGION_USA;
				break;
			case 'J':
				ConsoleRegion=CONSOLE_REGION_JAPAN;
		}

		result=ConsoleRegion;
	}

	return result;
}

int main(int argc, char *argv[]){
	char *args[3];
	t_ExecData ElfData;
	unsigned short int ROMVersion;

	SifInitRpc(0);
	fioInit();

	if (Read_SYSTEM_CNF(cdboot_path, ver) == 1) { // double check PS1 disc type & read CNF
		args[0] = "rom0:PS1DRV";
		args[1] = cdboot_path;  // 1st arg is main elf path
		args[2] = ver;          // 2nd arg is ver

		ROMVersion=GetBootROMVersion();
		SelectedVMode=(GetConsoleRegion()==CONSOLE_REGION_EUROPE)?2:3;	//Negate video mode: if region == Europe, set video mode to NTSC. Otherwise, PAL.

		FlushCache(0);

		SifLoadElf("rom0:PS1DRV", &ElfData);

		if(ROMVersion>0x200){
			HookUnpackFunction((void*)ElfData.epc);
		}
		else{
			ChangePS1DrvVideoMode();
		}

		SifExitRpc();
		fioExit();

		ExecPS2((void*)ElfData.epc, (void*)ElfData.gp, 3, args);
	}
	else{
		SifExitRpc();
		fioExit();
	}

	Exit(0);

	return 0;
}
