// Nintendont (kernel): GameCube Memory Card functions.
// Used by EXI.c.

#include "GCNCard.h"
#include "Config.h"
#include "debug.h"
#include "ff_utf8.h"

#include "DI.h"
#include "RealDI.h"
#include "ES.h"
#include "SI.h"
#include "BT.h"
#include "string.h"

#define IOS_OPEN_READ				0x01
#define IOS_OPEN_WRITE				0x02
#define IOS_OPEN_RW				(IOS_OPEN_READ | IOS_OPEN_WRITE)

typedef struct {
	s32 fd;
	s32 pos;
	int mode;   // IOS_OPEN_*
} IOS_FILE;

IOS_FILE* IOS_fopen(const char* path, const char* mode)
{
	IOS_FILE* f = malloc(sizeof(IOS_FILE));
	memset(f, 0, sizeof(IOS_FILE));
	if (!f) return NULL;

	if (!strcmp(mode, "r")) {
		f->mode = IOS_OPEN_READ;
	}
	else if (!strcmp(mode, "w")) {
		//IOS_CreateFile(path, 0, 3, 3, 3);
		f->mode = IOS_OPEN_WRITE;
	}
	else if (!strcmp(mode, "a")) {
		//IOS_CreateFile(path, 0, 3, 3, 3);
		f->mode = IOS_OPEN_RW;
	}
	else {
		free(f);
		return NULL;
	}

	f->fd = IOS_Open(path, f->mode);
	if (f->fd < 0) {
		free(f);
		return NULL;
	}

	if (!strcmp(mode, "a")) {
		f->pos = IOS_Seek(f->fd, 0, SEEK_END);
	}

	return f;
}

size_t IOS_fread(void* ptr, size_t size, size_t nmemb, IOS_FILE* f)
{
	if (!f || f->fd < 0) return 0;

	s32 bytes = IOS_Read(f->fd, ptr, size * nmemb);
	if (bytes < 0) return 0;

	f->pos += bytes;
	return bytes / size;
}

size_t IOS_fwrite(const void* ptr, size_t size, size_t nmemb, IOS_FILE* f)
{
	if (!f || f->fd < 0) return 0;

	s32 bytes = IOS_Write(f->fd, (void*)ptr, size * nmemb);
	if (bytes < 0) return 0;

	f->pos += bytes;
	return bytes / size;
}

int IOS_fseek(IOS_FILE* f, s32 offset, int whence)
{
	if (!f || f->fd < 0) return -1;

	s32 pos = IOS_Seek(f->fd, offset, whence);
	if (pos < 0) return -1;

	f->pos = pos;
	return 0;
}

int IOS_fclose(IOS_FILE* f)
{
	if (!f) return -1;

	if (f->fd >= 0)
		IOS_Close(f->fd);   // THIS is the flush

	free(f);
	return 0;
}

int IOS_expand(IOS_FILE* f, u32 size)
{
	if (!f || f->fd < 0)
		return -1;

	// Save current position
	s32 old_pos = IOS_Seek(f->fd, 0, SEEK_CUR);
	if (old_pos < 0)
		return old_pos;

	// Check current file size
	s32 end = IOS_Seek(f->fd, 0, SEEK_END);
	if (end < 0)
		return end;

	if ((u32)end < size) {
		// Extend file minimally
		IOS_Seek(f->fd, size - 1, SEEK_SET);
		u8 zero = 0;
		s32 ret = IOS_Write(f->fd, &zero, 1);
		if (ret != 1)
			return -1;
	}

	// Restore original position
	IOS_Seek(f->fd, old_pos, SEEK_SET);
	f->pos = old_pos;

	return 0;
}

// Triforce variables.
extern vu32 TRIGame;

// Memory Card context.
static u8 *const GCNCard_base = (u8*)(0x11000000);

typedef struct _GCNCard_ctx {
	char filename[0x20];    // Memory Card filename.
	u8 *base;               // Base address.
	u32 size;               // Size, in bytes.
	u32 code;               // Memory card "code".

	// BlockOffLow starts from 0xA000; does not include "system" blocks.
	// For system blocks, check 'changed_system'.
	bool changed;		// True if the card has been modified at all.
				// (NOTE: Reset after calling GCNCard_CheckChanges().)
	bool changed_system;	// True if the system area (first 5 blocks)
				// has been modified. These blocks are NOT
				// included in BlockOffLow / BlockOffHigh.

	// NOTE: BlockOff is in bytes, not blocks.
	u32 BlockOff;           // Current offset.
	u32 BlockOffLow;        // Low address of last modification.
	u32 BlockOffHigh;       // High address of last modification.
	u32 CARDWriteCount;     // Write count. (TODO: Is this used anywhere?)
} GCNCard_ctx;
#ifdef GCNCARD_ENABLE_SLOT_B
static GCNCard_ctx memCard[2] __attribute__((aligned(32)));
#else /* !GCNCARD_ENABLE_SLOT_B */
static GCNCard_ctx memCard[1] __attribute__((aligned(32)));
#endif /* GCNCARD_ENABLE_SLOT_B */

#define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))

static void GCNCard_InitCtx(GCNCard_ctx *ctx)
{
	memset(ctx, 0, sizeof(*ctx));
	ctx->BlockOffLow = 0xFFFFFFFF;
}

/**
 * Is a memory card enabled?
 * @param slot Slot number. (0 == Slot A, 1 == Slot B)
 * @return 0 if disabled; 1 if enabled.
 */
inline u32 GCNCard_IsEnabled(int slot)
{
	if (slot < 0 || slot >= ARRAY_SIZE(memCard))
		return 0;

	// Card is enabled if it's larger than 0 bytes.
	return (memCard[slot].size > 0);
}

/**
 * Load a memory card from disk.
 * @param slot Slot number. (0 == Slot A, 1 == Slot B)
 * NOTE: Slot B is not valid on Triforce.
 * @return 0 on success; non-zero on error.
 */

void logInfo(const char *msg)
{
	const char* file_path = "/title/00010001/69696969/data/nick.txt"; // Nick
	IOS_FILE* IOS_f = IOS_fopen(file_path, "a");
	IOS_fwrite(msg, 1, strlen(msg), IOS_f);
	IOS_fclose(IOS_f);
}

int GCNCard_Load(int slot)
{
	logInfo("GCNCard_Load called\r\n");
	/*
	const char* file_path = "/title/00010001/69696969/data/nick.txt"; // Nick
	IOS_FILE* IOS_f = IOS_fopen(file_path, "w");
	const char msg[] = "Real Hello NAND\n";
	IOS_fwrite(msg, 1, sizeof(msg)-1, IOS_f);
	IOS_fclose(IOS_f);*/

	// return 0;			// NICK!!!!!!!!!!!!!!!!!!
	switch (slot)
	{
		case 0:
			// Slot A
			break;
#ifdef GCNCARD_ENABLE_SLOT_B
		case 1:
			// Slot B (not valid on Triforce)
			if (TRIGame != 0)
				return -1;
			break;
#endif /* GCNCARD_ENABLE_SLOT_B */
		default:
			// Invalid slot.
			return -2;
	}

	// Get the Game ID.
	const u32 GameID = ConfigGetGameID();

	logInfo("GCNCard_Load called 2\r\n");

	// Set up the Memory Card context.
	GCNCard_ctx *const ctx = &memCard[slot];
	GCNCard_InitCtx(ctx);
	
	memset(ctx->filename, 0, 0x20);		// Nick
	char* fname_ptr = &ctx->filename[0];

	logInfo("GCNCard_Load called 3\r\n");

	//memcpy(ctx->filename, "/saves/", 7);
	//char *fname_ptr = &ctx->filename[7];

	if (ConfigGetConfig(NIN_CFG_MC_MULTI))
	{
		// "Multi" mode enabled. (one card for all saves, per region)
		memcpy(fname_ptr, "ninmem", 6);
		fname_ptr += 6;

		if (BI2region == BI2_REGION_JAPAN ||
		    BI2region == BI2_REGION_SOUTH_KOREA)
		{
			// JPN game. Append a 'j'.
			*fname_ptr++ = 'j';
		}

#ifdef GCNCARD_ENABLE_SLOT_B
		if (slot)
		{
			// Slot B. Append a 'b'.
			*fname_ptr++ = 'b';
		}
#endif /* GCNCARD_ENABLE_SLOT_B */

		// Append the file extension. (with NULL terminator)
		memcpy(fname_ptr, ".raw", 5);
	}
	else
	{
		// Single mode. One card per game.
		memcpy(fname_ptr, &GameID, 4);
		fname_ptr += 4;

#ifdef GCNCARD_ENABLE_SLOT_B
		if (slot)
		{
			// Slot B. Append "_B".
			*(fname_ptr+0) = '_';
			*(fname_ptr+1) = 'B';
			fname_ptr += 2;
		}
#endif /* GCNCARD_ENABLE_SLOT_B */

		// Append the file extension. (with NULL terminator)
		memcpy(fname_ptr, ".raw", 5);
	}

	logInfo("GCNCard_Load called 4\r\n");

	sync_after_write(ctx->filename, sizeof(ctx->filename));

	dbgprintf("EXI: Trying to open %s\r\n", ctx->filename);
	//FIL fd;
	//int ret = f_open_char(&fd, ctx->filename, FA_READ|FA_OPEN_EXISTING);
	char full_path[150];
	_sprintf(full_path, "/title/00010001/69696969/data/%s", ctx->filename);

	logInfo("GCNCard_Load called 5\r\n");
	logInfo(full_path);

	IOS_FILE* IOS_f = IOS_fopen(full_path, "r");

	logInfo("GCNCard_Load called 6\r\n");

	if (IOS_f == NULL)
	{
		logInfo("GCNCard_Load called 6 IOS_f is NULL\r\n");
#ifdef DEBUG_EXI
		dbgprintf("EXI: Slot %c: Failed to open %s: %u\r\n", (slot+'A'), ctx->filename, ret );
#endif
#ifdef GCNCARD_ENABLE_SLOT_B
		if (slot == 0)
		{
			// Slot A failure is fatal.
			Shutdown();
		}

		// Slot B failure will simply disable Slot B.
		dbgprintf("EXI: Slot %c has been disabled.\r\n", (slot+'A'));
		return -3;
#else /* !GCNCARD_ENABLE_SLOT_B */
		// Slot A failure is fatal.
		Shutdown();
#endif /* GCNCARD_ENABLE_SLOT_B */
	}

	IOS_fseek(IOS_f, 0, SEEK_END);
	s32 objsize = IOS_f->pos;
	IOS_fseek(IOS_f, 0, SEEK_SET);

#ifdef DEBUG_EXI
	dbgprintf("EXI: Loading memory card for Slot %c...", (slot+'A'));
#endif

	// Check if the card filesize is valid.
	u32 FindBlocks = 0;
	for (FindBlocks = 0; FindBlocks <= MEM_CARD_MAX; FindBlocks++)
	{
		if (MEM_CARD_SIZE(FindBlocks) == /*fd.obj.*/objsize)
			break;
	}
	if (FindBlocks > MEM_CARD_MAX)
	{
		logInfo("GCNCard_Load FindBlocks > MEM_CARD_MAX BAD \r\n");

		dbgprintf("EXI: Slot %c unexpected size %s: %u\r\n",
				(slot+'A'), ctx->filename, /*fd.obj.*/objsize);
#ifdef GCNCARD_ENABLE_SLOT_B
		if (slot == 0)
		{
			// Slot A failure is fatal.
			Shutdown();
		}

		// Slot B failure will simply disable Slot B.
		dbgprintf("EXI: Slot %c has been disabled.\r\n", (slot+'A'));
		IOS_fclose(IOS_f);
		//f_close(&fd);
		return -4;
#else /* !GCNCARD_ENABLE_SLOT_B */
		// Slot A failure is fatal.
		Shutdown();
#endif /* GCNCARD_ENABLE_SLOT_B */
	}

#if GCNCARD_ENABLE_SLOT_B
	if (slot == 0)
	{
		// Slot A starts at GCNCard_base.
		ctx->base = GCNCard_base;
		// Set the memory card size for Slot A only.
		ConfigSetMemcardBlocks(FindBlocks);
	}
	else
	{
		// Slot B starts immediately after Slot A.
		// Make sure both cards fit within 16 MB.
		if (memCard[0].size + /*fd.obj.*/objsize > (16*1024*1024))
		{
			// Not enough memory for both cards.
			// Disable Slot B.
			dbgprintf("EXI: Slot A is %u MB; not enough space for Slot %c, which is %u MB.\r\n",
					"EXI: Slot %c has been disabled.\r\n",
					memCard[0].size / 1024 / 1024, (slot+'A'),
					/*fd.obj.*/objsize / 1024 / 1024, (slot+'A'));
			//f_close(&fd);
			IOS_fclose(IOS_f);
			return -4;
		}
		ctx->base = memCard[0].base + memCard[0].size;
	}
#else /* !GCNCARD_ENABLE_SLOT_B */
	// Slot A starts at GCNCard_base.
	ctx->base = GCNCard_base;
	// Set the memory card size for Slot A only.
	ConfigSetMemcardBlocks(FindBlocks);
#endif /* GCNCARD_ENABLE_SLOT_B */

	logInfo("GCNCard_Load called 6\r\n");

	// Size and "code".
	ctx->size = /*fd.obj.*/objsize;
	ctx->code = MEM_CARD_CODE(FindBlocks);

	// Read the memory card contents into RAM.
	IOS_fseek(IOS_f, 0, SEEK_SET);
	IOS_fread(ctx->base, 1, ctx->size, IOS_f);
	IOS_fclose(IOS_f);

	logInfo("GCNCard_Load called 7\r\n");
	/*
	UINT read;
	f_lseek(&fd, 0);
	f_read(&fd, ctx->base, ctx->size, &read);
	f_close(&fd);
	*/
	// Reset the low/high offsets to indicate that everything was just loaded.
	ctx->BlockOffLow = 0xFFFFFFFF;
	ctx->BlockOffHigh = 0x00000000;

#ifdef DEBUG_EXI
	dbgprintf("EXI: Loaded Slot %c memory card size %u\r\n", (slot+'A'), ctx->size);
#endif

	// Synchronize the memory card data.
	sync_after_write(ctx->base, ctx->size);

	logInfo("GCNCard_Load called 8\r\n");

#ifdef GCNCARD_ENABLE_SLOT_B
	if (slot == 1)
	{
		// Slot B card image loaded successfully.
		ncfg->Config |= NIN_CFG_MC_SLOTB;
	}
#endif /* GCNCARD_ENABLE_SLOT_B */

	logInfo("GCNCard_Load called 10\r\n");

	return 0;
}

/**
* Get the total size of the loaded memory cards.
* @return Total size, in bytes.
*/
u32 GCNCard_GetTotalSize(void)
{
#ifdef GCNCARD_ENABLE_SLOT_B
	return (memCard[0].size + memCard[1].size);
#else /* !GCNCARD_ENABLE_SLOT_B */
	return memCard[0].size;
#endif /* GCNCARD_ENABLE_SLOT_B */
}

/**
* Check if the memory cards have changed.
* @return True if either memory card has changed; false if not.
*/
bool GCNCard_CheckChanges(void)
{
	int slot;
	bool ret = false;
	for (slot = 0; slot < ARRAY_SIZE(memCard); slot++)
	{
		// CHeck if the memory card is dirty in general.
		if (memCard[slot].changed)
		{
			memCard[slot].changed = false;
			ret = true;
		}
	}
	return ret;
}

/**
* Save the memory card(s).
*/
void GCNCard_Save(void)
{
	if (TRIGame)
	{
		// Triforce doesn't use the standard EXI CARD interface.
		return;
	}

	int slot;
	for (slot = 0; slot < ARRAY_SIZE(memCard); slot++)
	{
		if (!GCNCard_IsEnabled(slot))
		{
			// Card isn't initialized.
			continue;
		}

		// Does this card have any unsaved changes?
		GCNCard_ctx *const ctx = &memCard[slot];
		if (ctx->changed_system ||
		    ctx->BlockOffLow < ctx->BlockOffHigh)
		{
//#ifdef DEBUG_EXI
			//dbgprintf("EXI: Saving memory card in Slot %c...", (slot+'A'));
//#endif
//			FIL fd;
	//		int ret = f_open_char(&fd, ctx->filename, FA_WRITE|FA_OPEN_EXISTING);
			char full_path[170];
			_sprintf(full_path, "/title/00010001/69696969/data/%s", ctx->filename);
			IOS_FILE* IOS_f = IOS_fopen(full_path, "w");

			if (/*ret == FR_OK*/IOS_f)
			{
				//UINT wrote;
				sync_before_read(ctx->base, ctx->size);

				// Save the system area, if necessary.
				if (ctx->changed_system)
				{
					IOS_fseek(IOS_f, 0, SEEK_SET);
					IOS_fwrite(ctx->base, 1, 0xA000, IOS_f);
					//f_lseek(&fd, 0);
					//f_write(&fd, ctx->base, 0xA000, &wrote);
				}

				// Save the general area, if necessary.
				if (ctx->BlockOffLow < ctx->BlockOffHigh)
				{
					IOS_fseek(IOS_f, ctx->BlockOffLow, SEEK_SET);
					IOS_fwrite(&ctx->base[ctx->BlockOffLow], 1, (ctx->BlockOffHigh - ctx->BlockOffLow), IOS_f);

					/*
					f_lseek(&fd, ctx->BlockOffLow);
					f_write(&fd, &ctx->base[ctx->BlockOffLow],
						(ctx->BlockOffHigh - ctx->BlockOffLow), &wrote);*/
				}

				IOS_fclose(IOS_f);
				//f_close(&fd);
//#ifdef DEBUG_EXI
				//dbgprintf("Done!\r\n");
//#endif
			}
			else
			{
				dbgprintf("\r\nEXI: Unable to open Slot %c memory card file: %u\r\n", (slot+'A'), /*ret*/0);
			}

			// Reset the low/high offsets to indicate that everything has been saved.
			ctx->BlockOffLow = 0xFFFFFFFF;
			ctx->BlockOffHigh = 0x00000000;
			ctx->changed_system = false;
		}
	}
}

/** Functions used by EXIDeviceMemoryCard(). **/

void GCNCard_ClearWriteCount(int slot)
{
	if (!GCNCard_IsEnabled(slot))
		return;

	memCard[slot].CARDWriteCount = 0;
}

/**
 * Set the current block offset.
 * @param slot Slot number.
 * @param data Block offset from the EXI command.
 */
void GCNCard_SetBlockOffset(int slot, u32 data)
{
	if (!GCNCard_IsEnabled(slot))
		return;

	u32 BlockOff = ((data>>16)&0xFF)  << 17;
	BlockOff    |= ((data>> 8)&0xFF)  << 9;
	BlockOff    |= ((data&0xFF)  &3)  << 7;
	memCard[slot].BlockOff = BlockOff;
}

/**
 * Write data to the card using the current block offset.
 * @param slot Slot number.
 * @param data Data to write.
 * @param length Length of data to write, in bytes.
 */
void GCNCard_Write(int slot, const void *data, u32 length)
{
	if (!GCNCard_IsEnabled(slot))
		return;
	GCNCard_ctx *const ctx = &memCard[slot];
	ctx->changed = true;

	// Is this update entirely within the "system area"?
	if (ctx->BlockOff < 0xA000 && ctx->BlockOff + length < 0xA000)
	{
		// This update is entirely within the "system area".
		// Only set the flag; don't set block offsets.
		ctx->changed_system = true;
	}
	else
	{
		// Update the block offsets for saving.
		if (ctx->BlockOff < ctx->BlockOffLow)
			ctx->BlockOffLow = ctx->BlockOff;
		if (ctx->BlockOff + length > ctx->BlockOffHigh)
			ctx->BlockOffHigh = ctx->BlockOff + length;
		ctx->changed = true;

		if (ctx->BlockOff < 0xA000)
		{
			// System area as well as general area.
			ctx->changed_system = true;
		}
		if (ctx->BlockOffLow < 0xA000)
		{
			// BlockOffLow shouldn't be less than 0xA000.
			// Otherwise, we end up with double writing.
			// (Not a problem; just wastes time.)
			ctx->BlockOffLow = 0xA000;
			ctx->changed_system = true;
		}
	}

	// FIXME: Verify that this doesn't go out of bounds.
	sync_before_read((void*)data, length);
	memcpy(&ctx->base[ctx->BlockOff], data, length);
	sync_after_write(&ctx->base[ctx->BlockOff], length);
}

/**
 * Read data from the card using the current block offset.
 * @param slot Slot number.
 * @param data Buffer for the read data.
 * @param length Length of data to read, in bytes.
 */
void GCNCard_Read(int slot, void *data, u32 length)
{
	if (!GCNCard_IsEnabled(slot))
		return;
	GCNCard_ctx *const ctx = &memCard[slot];

	// FIXME: Verify that this doesn't go out of bounds.
	sync_before_read(&ctx->base[ctx->BlockOff], length);
	memcpy(data, &ctx->base[ctx->BlockOff], length);
	sync_after_write(data, length);
}

/**
 * Get the card's "code" value.
 * @param slot Slot number.
 * @param Card's "code" value.
 */
u32 GCNCard_GetCode(int slot)
{
	if (!GCNCard_IsEnabled(slot))
		return 0;
	return memCard[slot].code;
}

/**
 * Set the current block offset. (ERASE mode; uses sector values only.)
 * @param slot Slot number.
 * @param Data Block offset (sector values) from the EXI command.
 */
void GCNCard_SetBlockOffset_Erase(int slot, u32 data)
{
	if (!GCNCard_IsEnabled(slot))
		return;

	u32 BlockOff = (((u32)data>>16)&0xFF)  << 17;
	BlockOff    |= (((u32)data>> 8)&0xFF)  << 9;
	memCard[slot].BlockOff = BlockOff;
}

#ifdef DEBUG_EXI
/**
 * Get the current block offset. (decoded value, for debugging purposes)
 * @param slot Slot number.
 * @return Block offset, decoded.
 */
u32 GCNCard_GetBlockOffset(int slot)
{
	if (!GCNCard_IsEnabled(slot))
		return 0;
	return memCard[slot].BlockOff;
}
#endif
