/***************************************************************************
 * Copyright (C) 2012
 * by OverjoY and FIX94 for Wiiflow
 *
 * This software is provided 'as-is', without any express or implied
 * warranty. In no event will the authors be held liable for any
 * damages arising from the use of this software.
 *
 * Permission is granted to anyone to use this software for any
 * purpose, including commercial applications, and to alter it and
 * redistribute it freely, subject to the following restrictions:
 *
 * 1. The origin of this software must not be misrepresented; you
 * must not claim that you wrote the original software. If you use
 * this software in a product, an acknowledgment in the product
 * documentation would be appreciated but is not required.
 *
 * 2. Altered source versions must be plainly marked as such, and
 * must not be misrepresented as being the original software.
 *
 * 3. This notice may not be removed or altered from any source
 * distribution.
 *
 * gc_disc_dump.cpp
 *
 ***************************************************************************/
#include <stdio.h>
#include <unistd.h>
#include <ogcsys.h>
#include <sys/statvfs.h>

#include "gc_disc_dump.hpp"
#include "disc.h"
#include "utils.h"
#include "wdvd.h"
#include "devicemounter/DeviceHandler.hpp"
#include "fileOps/fileOps.h"
#include "gecko/gecko.hpp"
#include "hw/Gekko.h"
#include "gui/text.hpp"
#include "memory/mem2.hpp"
#include "menu/menu.hpp"

static u8 *FSTable ATTRIBUTE_ALIGN(32);

void GCDump::__AnalizeMultiDisc()
{
	u8 *Buffer = (u8 *)MEM2_memalign(32, ALIGN32(0x10));
	if(Buffer == NULL)
		return;

	MultiGameCnt = 0;
	while(1)
	{
		__DiscReadRaw(Buffer, 0x40+(MultiGameCnt*4), 4);
		u64 MultiGameOffset = *(vu32*)(Buffer) << 2;
		if(!MultiGameOffset)
			break;
		
		if(MultiGameDump == MultiGameCnt)
			NextOffset = MultiGameOffset;
		MultiGameCnt++;
	}
	MEM2_free(Buffer);
}

s32 GCDump::__DiscReadRaw(void *outbuf, u64 offset, u32 length)
{
	length = ALIGN32(length);
	wiiLightOn();
	while(1)
	{
		gc_error = 0;
		s32 ret = WDVD_UnencryptedRead(outbuf, length, offset);
		if(ret != 0)
		{
			WDVD_LowRequestError(&gc_error);
			if(gc_error == 0x30200 || gc_error == 0x30201 || gc_error == 0x31100)
			{
				if(gc_retry >= gc_nbrretry && waitonerror)
				{
					__WaitForDisc(Disc, 10);
					gc_retry = 0;
					waitonerror = false;
					if(FSTTotal > FSTSize)
						mainMenu.GC_Messenger(4, Disc+1, minfo);
					else
						mainMenu.GC_Messenger(3, 0, minfo);
				}
				if(gc_retry >= gc_nbrretry)
				{
					wiiLightOff();
					if(!skiponerror)
						return gc_error;
					else
					{
						gc_retry = 0;
						gc_skipped++;
						gprintf("Read error (%x) at offset: 0x%08x. Skipping %d bytes\n", gc_error, offset, length);
						return 1;
					}
				}
				gc_retry++;
			}
			else if(gc_error == 0x1023a00)
			{
				__WaitForDisc(Disc, 11);
				if(FSTTotal > FSTSize)
					mainMenu.GC_Messenger(4, Disc+1, minfo);
				else
					mainMenu.GC_Messenger(3, 0, minfo);
			}
			else
			{
				gprintf("Read error(%x) at offset: 0x%08x.\n", gc_error, offset);
				wiiLightOff();
				return 0;
			}
		}
		else
		{
			wiiLightOff();
			gc_retry = 0;
			return ret;
		}
	}
	wiiLightOff();
	return -1;
}

s32 GCDump::__DiscWrite(char * path, u64 offset, u32 length, u8 *ReadBuffer)
{
	gprintf("__DiscWrite(%s, 0x%08x, %x)\n", path, offset, length);
	u32 wrote = 0;
	FILE *f = fopen(path, "wb");

	wrote = __DiscWriteFile(f, offset, length, ReadBuffer);
	
	fclose(f);
	return wrote;
}

s32 GCDump::__DiscWriteFile(FILE *f, u64 offset, u32 length, u8 *ReadBuffer)
{
	u32 toread = 0;
	u32 wrote = 0;

	while(length)
	{
		toread = min(length, gc_readsize);
		mainMenu.update_pThread(toread);
		s32 ret = __DiscReadRaw(ReadBuffer, offset, toread);
		if (ret == 1)
			memset(ReadBuffer, 0, gc_readsize);
		else if (ret > 1)
			return 0;
		fwrite(ReadBuffer, 1, toread, f);
		wrote += toread;
		offset += toread;
		length -= toread;
		gc_done += toread;
	}
	return wrote;
}

bool GCDump::__WaitForDisc(u8 dsc, u32 msg)
{
	u8 *ReadBuffer = (u8 *)MEM2_memalign(32, ALIGN32(0x440));
	if(ReadBuffer == NULL)
		return false;

	u32 cover = 0;
	bool done = false;
	while(!done)
	{
		mainMenu.GC_Messenger(msg, dsc+1, minfo);
		while(1)
		{
			wiiLightOn();
			usleep(1000000);
			wiiLightOff();
			usleep(1000000);
			WDVD_GetCoverStatus(&cover);
			if(!(cover & 0x2))
				break;
		}

		while(1)
		{
			wiiLightOn();
			usleep(1000000);
			wiiLightOff();
			usleep(1000000);
			if(Disc_Wait() < 0)
				continue;

			if(Disc_Open(false) < 0)
			{
				MEM2_free(ReadBuffer);
				return false;
			}

			if(Disc_IsGC() == 0)
			{
				s32 ret = __DiscReadRaw(ReadBuffer, NextOffset, 0x440);
				if(ret > 0)
				{
					MEM2_free(ReadBuffer);
					return false;
				}

				ID2 = *(vu32*)(ReadBuffer);	
				Disc2 = *(vu8*)(ReadBuffer+0x06);

				if(ID == ID2 && Disc2 == dsc)
				{
					done = true;
					break;
				}
				else if(ID == ID2 && Disc2 != dsc)
				{
					mainMenu.GC_Messenger( 7, Disc2+1, NULL);
					usleep( 5000000 );
					break;
				}
				else if(ID != ID2)
				{
					mainMenu.GC_Messenger( 8, 0, NULL);
					usleep( 5000000 );
					break;
				}
			}
			else if(Disc_IsWii() == 0)
			{
				mainMenu.GC_Messenger( 5, 0, NULL);
				usleep( 5000000 );
				break;
			}
			else
			{
				mainMenu.GC_Messenger( 6, 0, NULL);
				usleep( 5000000 );
				break;
				
			}
		}
	}
	MEM2_free(ReadBuffer);
	return done;
}

bool GCDump::__CheckMDHack(u32 ID)
{
	/********************************************************************/
	/**** Because some lazy gamedevs don't set a proper max FST size ****/
	/*** on their first disc some games need a hardcoded hack. Please ***/
	/****** report unsupported games so we can add an entry for it ******/
	/********************************************************************/
	switch(ID >> 8)
	{
		case 0x475153: /*** Tales of Symphonia ***/
		case 0x474b42: /*** Baten Kaitos: Eternal Wings and the Lost Ocean ***/
		case 0x473341: /*** The Lord of the Rings: The Third Age ***/
		case 0x473554: /*** Tiger Woods PGA Tour 2005 ***/
		case 0x473442: /*** Resident Evil 4 ***/
			return true;
		break;
		default:
			return false;
		break;
	}
	return false;
}

s32 GCDump::DumpGame()
{
	u8 *ReadBuffer = (u8 *)MEM2_memalign(32, ALIGN32(gc_readsize));
	if(ReadBuffer == NULL)
		return 0x31100;

	gc_done = 0;
	gamedone = false;
	multigamedisc = false;
	MultiGameDump = 0;
	MultiGameCnt = 0;
	NextOffset = 0;
	Disc = 0;

	char *FSTNameOff = NULL;

	char folder[MAX_FAT_PATH + 76];
	memset(folder, 0, sizeof(folder));
	char gamepath[MAX_FAT_PATH + 90];
	memset(gamepath, 0, sizeof(gamepath));
	char basedir[MAX_FAT_PATH];
	memset(basedir, 0, sizeof(basedir));
	strcpy(basedir, fmt(gc_gamesDir, gamepartition));

	while(!gamedone)
	{
		u8 *FSTBuffer;
		u32 wrote = 0;
		memset(&gc_hdr, 0, sizeof(gc_hdr));
		s32 ret = Disc_ReadGCHeader(&gc_hdr);
		if(memcmp(gc_hdr.id, "GCOPDV", 6) == 0)
		{
			multigamedisc = true;
			__AnalizeMultiDisc();
			__DiscReadRaw(ReadBuffer, NextOffset, sizeof(gc_discHdr));
			memcpy(gc_hdr.title, ReadBuffer + 0x20, 64);
			memcpy(gc_hdr.id, ReadBuffer, 6);
		}
		Asciify2(gc_hdr.title);

		snprintf(folder, sizeof(folder), basedir);
		fsop_MakeFolder(folder);

		memset(folder, 0, sizeof(folder));
		snprintf(folder, sizeof(folder), "%s/%s [%.06s]", basedir, gc_hdr.title, gc_hdr.id);
		if(!fsop_FolderExist(folder))
			fsop_MakeFolder(folder);
		else if(!Disc)
		{
			gprintf("Skipping game: %s (Already installed)(%d)\n", gc_hdr.title, Gamesize[MultiGameDump]);
			break;
		}

		ret = __DiscReadRaw(ReadBuffer, NextOffset, 0x440);
		if(ret > 0)
		{
			MEM2_free(ReadBuffer);
			return 0x31100;
		}

		ID = *(vu32*)(ReadBuffer);
		Disc = *(vu8*)(ReadBuffer+0x06);
		ApploaderSize = *(vu32*)(ReadBuffer+0x400);
		DOLOffset = *(vu32*)(ReadBuffer+0x420);
		FSTOffset = *(vu32*)(ReadBuffer+0x424);
		FSTSize = *(vu32*)(ReadBuffer+0x428);
		FSTTotal = *(vu32*)(ReadBuffer+0x42c);
		GamePartOffset = *(vu32*)(ReadBuffer+0x434);
		DataSize = *(vu32*)(ReadBuffer+0x438);

		DOLSize = FSTOffset - DOLOffset;
		DiscSize = DataSize + GamePartOffset;

		FSTBuffer = (u8 *)MEM2_memalign(32, ALIGN32(FSTSize));
		if(FSTBuffer == NULL)
			return 0x31100;

		ret = __DiscReadRaw(FSTBuffer, FSTOffset+NextOffset, FSTSize);

		if(ret > 0)
		{
			MEM2_free(FSTBuffer);
			MEM2_free(ReadBuffer);
			return 0x31100;
		}

		FSTable = (u8*)FSTBuffer;

		FSTEnt = *(u32*)(FSTable+0x08);

		FSTNameOff = (char*)(FSTable + FSTEnt * 0x0C);
		FST *fst = (FST *)(FSTable);

		snprintf(minfo, sizeof(minfo), "[%.06s] %s", gc_hdr.id, gc_hdr.title);

		if(FSTTotal > FSTSize)
			mainMenu.GC_Messenger(4, Disc+1, minfo);
		else
			mainMenu.GC_Messenger(3, 0, minfo);

		gprintf("Dumping: %s %s\n", gc_hdr.title, compressed ? "compressed" : "full");

		gprintf("Apploader size : %d\n", ApploaderSize);
		gprintf("DOL offset     : 0x%08x\n", DOLOffset);
		gprintf("DOL size       : %d\n", DOLSize);
		gprintf("FST offset     : 0x%08x\n", FSTOffset);
		gprintf("FST size       : %d\n", FSTSize);
		gprintf("Num FST entries: %d\n", FSTEnt);
		gprintf("Data Offset    : 0x%08x\n", FSTOffset+FSTSize);
		gprintf("Disc size      : %d\n", DiscSize);

		if(writeexfiles && !Disc)
		{
			memset(folder, 0, sizeof(folder));
			snprintf(folder, sizeof(folder), "%s/%s [%.06s]/sys", basedir, gc_hdr.title, gc_hdr.id);
			fsop_MakeFolder(folder);

			gprintf("Writing %s/boot.bin\n", folder);
			snprintf(gamepath, sizeof(gamepath), "%s/boot.bin", folder);
			gc_done += __DiscWrite(gamepath, NextOffset, 0x440, ReadBuffer);

			gprintf("Writing %s/bi2.bin\n", folder);
			snprintf(gamepath, sizeof(gamepath), "%s/bi2.bin", folder);
			gc_done += __DiscWrite(gamepath, 0x440+NextOffset, 0x2000, ReadBuffer);

			gprintf("Writing %s/apploader.img\n", folder);
			snprintf(gamepath, sizeof(gamepath), "%s/apploader.img", folder);
			gc_done += __DiscWrite(gamepath, 0x2440+NextOffset, ApploaderSize, ReadBuffer);
		}

		snprintf(gamepath, sizeof(gamepath), "%s/%s [%.06s]/game.iso", basedir, gc_hdr.title, gc_hdr.id);
		if(Disc)
		{
			char *ptz = strstr(gamepath, "game.iso");
			if(ptz != NULL)
				memcpy(ptz, "gam1.iso", 8);
		}
		gprintf("Writing %s\n", gamepath);
		if(compressed)
		{
			u32 align;
			u32 correction;
			u32 toread;

			FILE *f = fopen(gamepath, "wb");

			ret = __DiscWriteFile(f, NextOffset, (FSTOffset + FSTSize), ReadBuffer);
			wrote += (FSTOffset + FSTSize);
			gc_done += wrote;
	
			u32 i;

			for( i=1; i < FSTEnt; ++i )
			{
				if(fst[i].Type)
					continue;
				else
				{
					for(align = 0x8000; align > 2; align/=2)
					{
						if((fst[i].FileOffset & (align-1)) == 0 || force_32k_align)
						{
							correction = 0;
							while(((wrote+correction) & (align-1)) != 0)
								correction++;
							wrote += correction;
							while(correction)
							{
								toread=min(correction, gc_readsize);
								memset(ReadBuffer, 0, toread);
								fwrite(ReadBuffer, 1, toread, f);
								correction -= toread;
								gc_done += toread;
							}
							break;
						}
					}
					ret = __DiscWriteFile(f, fst[i].FileOffset+NextOffset, fst[i].FileLength, ReadBuffer);
					gprintf("Writing: %d/%d: %s from 0x%08x to 0x%08x(%i)\n", i, FSTEnt, FSTNameOff + fst[i].NameOffset, fst[i].FileOffset, wrote, align);
					if( ret >= 0 )
					{
						fst[i].FileOffset = wrote;
						wrote += ret;
					}
					else
					{
						MEM2_free(ReadBuffer);
						MEM2_free(FSTBuffer);
						fclose(f);
						return gc_error;
					}
				}
			}

			gprintf("Updating FST\n");
			fseek(f, FSTOffset, SEEK_SET);
			fwrite(fst, 1, FSTSize, f);
			fclose(f);

			gprintf("Done!! Disc old size: %d, disc new size: %d, saved: %d\n", DiscSize, wrote, DiscSize - wrote);
		}
		else
		{
			ret = __DiscWrite(gamepath, NextOffset, DiscSize, ReadBuffer);
			if( ret < 0 )
			{
				MEM2_free(ReadBuffer);
				MEM2_free(FSTBuffer);
				return gc_error;
			}
			gprintf("Done!! Disc size: %d\n", DiscSize);
		}
		MEM2_free(FSTBuffer);

		if((FSTTotal > FSTSize || __CheckMDHack(ID)) && !multigamedisc)
		{
			if(Disc)
			{
				gamedone = true;
				break;
			}
			else
			{
				__WaitForDisc(1, 9);
				Disc++;
			}
		}
		else if(multigamedisc)
		{
			if(MultiGameDump+1 == MultiGameCnt)
			{
				gamedone = true;
				break;
			}
			else
			{
				MultiGameDump++;
			}
		}
		else
			gamedone = true;
	}
	MEM2_free(ReadBuffer);

	return gc_skipped;
}

s32 GCDump::CheckSpace(u32 *needed, bool comp)
{
	u8 *ReadBuffer = (u8 *)MEM2_memalign(32, ALIGN32(0x440));
	if(ReadBuffer == NULL)
		return 1;

	u32 size = 0;
	bool scnddisc = false;
	gamedone = false;
	multigamedisc = false;
	MultiGameDump = 0;
	MultiGameCnt = 0;
	NextOffset = 0;
	Disc = 0;

	while(!gamedone)
	{
		u32 multisize = 0;
		memset(&gc_hdr, 0, sizeof(gc_hdr));
		Disc_ReadGCHeader(&gc_hdr);
		if(memcmp(gc_hdr.id, "GCOPDV", 6) == 0)
		{
			multigamedisc = true;
			__AnalizeMultiDisc();
			__DiscReadRaw(ReadBuffer, NextOffset, sizeof(gc_discHdr));
			memcpy(gc_hdr.title, ReadBuffer + 0x20, 64);
			memcpy(gc_hdr.id, ReadBuffer, 6);
		}
		Asciify2(gc_hdr.title);

		s32 ret = __DiscReadRaw(ReadBuffer, NextOffset, 0x440);
		if(ret > 0)
		{
			MEM2_free(ReadBuffer);
			return 1;
		}

		ID = *(vu32*)(ReadBuffer);
		ID2 = 0;
		Disc = *(vu8*)(ReadBuffer+0x06);
		Disc2 = 0;
		ApploaderSize = *(vu32*)(ReadBuffer+0x400);
		FSTOffset = *(vu32*)(ReadBuffer+0x424);
		FSTSize = *(vu32*)(ReadBuffer+0x428);
		FSTTotal = *(vu32*)(ReadBuffer+0x42c);
		GamePartOffset = *(vu32*)(ReadBuffer+0x434);
		DataSize = *(vu32*)(ReadBuffer+0x438);
		DiscSize =  DataSize + GamePartOffset;

		snprintf(minfo, sizeof(minfo), "[%.06s] %s", gc_hdr.id, gc_hdr.title);

		mainMenu.GC_Messenger(2, 0, minfo);

		if(writeexfiles)
		{
			multisize += 0xa440;
			multisize += ApploaderSize;
		}

		if(!comp)
		{
			multisize += DiscSize;
		}
		else
		{
			u8 *FSTBuffer = (u8*)MEM2_memalign(32, ALIGN32(FSTSize));
			if(FSTBuffer == NULL)
				return 1;

			ret = __DiscReadRaw(FSTBuffer, FSTOffset+NextOffset, FSTSize);
			if(ret > 0)
			{
				MEM2_free(FSTBuffer);
				return 1;
			}

			FSTable = (u8*)FSTBuffer;
			u32 FSTEnt = *(u32*)(FSTable+0x08);
			FST *fst = (FST *)(FSTable);

			multisize += (FSTOffset + FSTSize);

			u32 i;
			u32 correction;
			for(i = 1; i < FSTEnt; ++i)
			{
				if(fst[i].Type)
					continue;
				else
				{
					for(u32 align = 0x8000; align > 2; align /= 2)
					{
						if((fst[i].FileOffset & (align - 1)) == 0 || force_32k_align)
						{
							correction = 0;
							while(((multisize+correction) & (align-1)) != 0)
								correction++;
							multisize += correction;
							break;
						}
					}
					multisize += fst[i].FileLength;
				}
			}
			MEM2_free(FSTBuffer);
		}
		size += multisize;
		Gamesize[MultiGameDump] = multisize;

		if((FSTTotal > FSTSize || __CheckMDHack(ID)) && !multigamedisc)
		{
			if(Disc == 0 && !scnddisc)
				__WaitForDisc(1, 1);
			else if(Disc == 0x01 && !scnddisc)
				__WaitForDisc(0, 1);

			if(scnddisc)
			{
				if(Disc == 0x01)
					__WaitForDisc(0, 1);

				gamedone = true;
				break;
			}
			scnddisc = true;
		}
		else if(multigamedisc)
		{
			if(MultiGameDump+1 == MultiGameCnt)
			{
				gamedone = true;
				break;
			}
			else
			{
				MultiGameDump++;
			}
		}
		else
			gamedone = true;
	}
	MEM2_free(ReadBuffer);
	mainMenu.m_thrdTotal = size;
	DiscSizeCalculated = size/0x400;
	*needed = (size/0x8000) >> 2;
	gprintf("Free space needed: %d Mb (%d blocks)\n", size/0x100000, (size/0x8000) >> 2);
	return 0;
}

u32 GCDump::GetFreeSpace(char *path, u32 Value)
{
	struct statvfs stats;
	memset(&stats, 0, sizeof(stats));
	statvfs(path , &stats);
	
	u64 free = (u64)stats.f_frsize * (u64)stats.f_bfree;
	
	switch(Value)
	{
		case KB:
			return free/0x400;
		case BL:
			return (free/0x8000) >> 2;
		case MB:
			return free/0x100000;
		case GB:
			return free/0x40000000;
	}	
	return 0;
}

