/* FCE Ultra - NES/Famicom Emulator
 *
 * Copyright notice for this file:
 *  Copyright (C) 2011 CaH4e3
 *
 * 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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

#ifdef COPYFAMI

/* *** COPY FAMICOM HARDWARE INTERFACE *** */

#define MESSAGE_LOG
#define NO_CACHE
#define NO_RAM

#include "__serial.h"
#include "mapinc.h"

#define FNV_32_PRIME    ((uint32)0x01000193)

#define CHR_CACHE_SIZE  (1024 * 4)
#define WRAM_CACHE_SIZE (1024 / 2)
#define PRG_CACHE_SIZE  (1024 / 4)
#define CMD_CACHE_SIZE  (1024 * 128)

#define CMD_MAX_SIZE    (5)
#define CMD_MAX_RETEST  (16)
#define CMD_MAX_VERIFY  (16)

static uint8 *WRAM = NULL;

uint8 InitVector[] = { 0xDE, 0xAD, 0xBE, 0xEF };/* args none,               return DE AD BE EF		*/
uint8 ResetCmd[] = { 0x00 };					/* args none,               return none				*/
uint8 StateCmd[] = { 0x01 };					/* args none,               return 7 bytes status	*/
uint8 StatusCmd[] = { 0x02 };					/* args none,               return 32 bytes status	*/
uint8 LoadPlugCmd[] = { 0x03, 0x00, 0x00 };		/* args 2b size, Nb data    return none				*/
uint8 RunPlugCmd[] = { 0x04 };					/* args none,               return none				*/
uint8 RunGameCmd[] = { 0x05 };					/* args none,               return none				*/
uint8 NROMSave[] = { 0x06 };					/* args none,               return 16b + 32kb + 8kb	*/

uint8 PRGWBCmd[] = { 0x08, 0x00, 0x00, 0x00 };	/* args 2b addr, 1b data    return none				*/
uint8 PRGRBCmd[] = { 0x09, 0x00, 0x00 };		/* args 2b addr             return 1b data			*/
uint8 CHRWBCmd[] = { 0x0A, 0x00, 0x00, 0x00 };	/* args 2b addr, 1b data    return none				*/
uint8 CHRRBCmd[] = { 0x0B, 0x00, 0x00 };		/* args 2b addr,            return 1b data			*/

uint8 PRGSUMCmd[] = { 0x10, 0x00, 0x00 };		/* args 1b addr, 1b size    return (256 * N)b		*/
uint8 PRG32KSUMCmd[] = { 0x10, 0x80, 0x80 };	/* args 1b addr, 1b size    return 32kb				*/
uint8 PRG16KSUMCmd[] = { 0x10, 0x00, 0x40 };	/* args 1b addr, 1b size    return 16kb				*/
uint8 PRG8KSUMCmd[] = { 0x10, 0x00, 0x20 };		/* args 1b addr, 1b size    return 8kb				*/
uint8 PRG4KSUMCmd[] = { 0x10, 0x00, 0x10 };		/* args 1b addr, 1b size    return 4kb				*/

uint8 CHRSUMCmd[] = { 0x11, 0x00, 0x00 };		/* args 1b addr, 1b size    return (256 * N)b		*/
uint8 CHR8KSUMCmd[] = { 0x11, 0x00, 0x20 };		/* args 1b addr, 1b size    return 8kb				*/
uint8 CHR4KSUMCmd[] = { 0x11, 0x00, 0x10 };		/* args 1b addr, 1b size    return 4kb				*/
uint8 CHR2KSUMCmd[] = { 0x11, 0x00, 0x08 };		/* args 1b addr, 1b size    return 2kb				*/
uint8 CHR1KSUMCmd[] = { 0x11, 0x00, 0x04 };		/* args 1b addr, 1b size    return 1kb				*/

uint8 PRGGetCmd[] = { 0x12, 0x00, 0x00 };		/* args 1b addr, 1b size    return (256 * N)b		*/
uint8 PRG32KGetCmd[] = { 0x12, 0x80, 0x80 };	/* args 1b addr, 1b size    return 32kb				*/
uint8 PRG16KGetCmd[] = { 0x12, 0x00, 0x40 };	/* args 1b addr, 1b size    return 16kb				*/
uint8 PRG8KGetCmd[] = { 0x12, 0x00, 0x20 };		/* args 1b addr, 1b size    return 8kb				*/
uint8 PRG4KGetCmd[] = { 0x12, 0x00, 0x10 };		/* args 1b addr, 1b size    return 4kb				*/

uint8 CHRGetCmd[] = { 0x13, 0x00, 0x00 };		/* args 1b addr, 1b size    return (256 * N)b		*/
uint8 CHR8KGetCmd[] = { 0x13, 0x00, 0x20 };		/* args 1b addr, 1b size    return 8kb				*/
uint8 CHR4KGetCmd[] = { 0x13, 0x00, 0x10 };		/* args 1b addr, 1b size    return 4kb				*/
uint8 CHR2KGetCmd[] = { 0x13, 0x00, 0x08 };		/* args 1b addr, 1b size    return 2kb				*/
uint8 CHR1KGetCmd[] = { 0x13, 0x00, 0x04 };		/* args 1b addr, 1b size    return 1kb				*/

uint8 CPUTestCmd[] = { 0x14, 0x00, 0x00 };		/* args 1b addr, 1b size    return (2b + 1b) * N + 3b	*/

typedef struct {
	int32 mirror;
	int32 chrsum[8];
	int32 prgsum[4];
} SYNC_STATE;

static SYNC_STATE state_cur, state_new, state_def;

typedef struct {
	uint8 *buf;
	int32 count;
} DATA_BANKS;

static DATA_BANKS chr_data;
static int32 chr_bank[0x10000];
static DATA_BANKS prg_data;
static int32 prg_bank[0x10000];

typedef struct {
	SYNC_STATE states[CMD_CACHE_SIZE];
	int32 seqs[CMD_CACHE_SIZE][CMD_MAX_SIZE];
	int32 count;
} SYNC_CMDS;

typedef struct {
	int32 seq[CMD_MAX_SIZE];
	int32 size;
	int32 found;
	uint32 hash;
	uint16 hashf;
} SYNC_CMD;

static SYNC_CMD cmd;
static SYNC_CMDS cmds;

typedef struct {
	int32 index;
	int32 size;
	int32 retest;
	int32 verify;
} CMD_CACHE;

static CMD_CACHE cmd_cache[0x10000];

static SFORMAT StateRegs[] =
{
	{ state_cur.chrsum, sizeof(state_cur.chrsum), "CHRREG" },
	{ state_cur.prgsum, sizeof(state_cur.prgsum), "ROMREG" },
	{ &state_cur.mirror, sizeof(state_cur.mirror), "MIRREG" },
	{ 0 }
};

#define MI_U 4

static char *mirror_names[5] = { "Horizontal", "Vertical", "Mirror 0", "Mirror 1", "Unknown mirror" };
static int32 mirror_modes[16] = {
	MI_0, MI_U, MI_U, MI_H, MI_U, MI_V, MI_U, MI_U,
	MI_U, MI_U, MI_U, MI_U, MI_U, MI_U, MI_U, MI_1
};

#define CHRDEF(slot)   (chr_bank[state_def.chrsum[slot]])
#define PRGDEF(slot)   (prg_bank[state_def.prgsum[slot]])
#define CHRCUR(slot)   (chr_bank[state_cur.chrsum[slot]])
#define PRGCUR(slot)   (prg_bank[state_cur.prgsum[slot]])
#define CHRNEW(slot)   (chr_bank[state_new.chrsum[slot]])
#define PRGNEW(slot)   (prg_bank[state_new.prgsum[slot]])

static void GetStatus(SYNC_STATE *state) {
	uint8 resp0;
	uint16 resp1, i;
	SEND(StatusCmd);
	GET(resp0, 1);
	state->mirror = resp0;
	GET(resp0, 1);
	for (i = 0; i < 8; i++) {
		GET(resp1, 2);
		state->chrsum[i] = resp1;
	}
	for (i = 0; i < 4; i++) {
		GET(resp1, 2);
		state->prgsum[i] = resp1;
	}
}

static int32 FetchNewCHRBank(int32 slot) {
	FILE *ofile;
	char name[256];
	int32 bank = chr_data.count++;
	CHR1KGetCmd[1] = slot << 2;
	SENDGET(CHR1KGetCmd, chr_data.buf[bank * 1024], 1024);
	sprintf(name, "%04x.chr", bank);
	ofile = fopen(name, "wb");
	fwrite((void*)&chr_data.buf[bank * 1024], 1, 1024, ofile);
	fclose(ofile);
	return bank;
}

static int32 FetchNewPRGBank(int32 slot) {
	FILE *ofile;
	char name[256];
	int32 bank = prg_data.count++;
	PRG8KGetCmd[1] = 0x80 + (slot << 5);
	SENDGET(PRG8KGetCmd, prg_data.buf[bank * 8192], 8192);
	sprintf(name, "%04x.prg", bank);
	ofile = fopen(name, "wb");
	fwrite((void*)&prg_data.buf[bank * 8192], 1, 8192, ofile);
	fclose(ofile);
	return bank;
}

static int CheckStatus(void) {
	int32 i, ischanged = 0;
	GetStatus(&state_new);
	if (state_cur.mirror != state_new.mirror) {
		state_cur.mirror = state_new.mirror;
#ifdef MESSAGE_LOG
		FCEU_printf(">> mirror changed to %s (%02X)\n", mirror_names[mirror_modes[state_cur.mirror]], state_cur.mirror);
#endif
		ischanged = 1;
	} else {
		state_new.mirror = -1;
	}
	for (i = 0; i < 8; i++) {
		if (state_cur.chrsum[i] != state_new.chrsum[i]) {
			state_cur.chrsum[i] = state_new.chrsum[i];
			if (CHRCUR(i) == -1) {
				CHRCUR(i) = FetchNewCHRBank(i);
#ifdef MESSAGE_LOG
				FCEU_printf(">> chr[%d] bank %d loaded\n", i, CHRCUR(i));
#endif
			}
#ifdef MESSAGE_LOG
			else
				FCEU_printf(">> chr[%d] bank %d switched\n", i, CHRCUR(i));
#endif
			ischanged = 1;
		} else {
			state_new.chrsum[i] = -1;
		}
	}
	for (i = 0; i < 4; i++) {
		if (state_cur.prgsum[i] != state_new.prgsum[i]) {
			state_cur.prgsum[i] = state_new.prgsum[i];
			if (PRGCUR(i) == -1) {
				PRGCUR(i) = FetchNewPRGBank(i);
#ifdef MESSAGE_LOG
				FCEU_printf(">> prg[%d] bank %d loaded\n", i, PRGCUR(i));
#endif
			}
#ifdef MESSAGE_LOG
			else
				FCEU_printf(">> prg[%d] bank %d switched\n", i, PRGCUR(i));
#endif
			ischanged = 1;
		} else {
			state_new.prgsum[i] = -1;
		}
	}
	return ischanged;
}

#ifndef NO_CACHE
static void ApplyStatus() {
	int32 i;
	if ((cmds.states[cmd.found].mirror != -1) && (cmds.states[cmd.found].mirror != state_cur.mirror)) {
		state_cur.mirror = cmds.states[cmd.found].mirror;
		setmirror(mirror_modes[state_cur.mirror]);
#ifdef MESSAGE_LOG
		FCEU_printf(">> mirror changed to %s (%02X)\n", mirror_names[mirror_modes[state_cur.mirror]], state_cur.mirror);
#endif
	}
	for (i = 0; i < 8; i++) {
		int32 sum = cmds.states[cmd.found].chrsum[i];
		if (sum != -1) {
			if (sum != state_cur.chrsum[i]) {
				state_cur.chrsum[i] = sum;
				setchr1r(1, i * 1024, CHRCUR(i));
#ifdef MESSAGE_LOG
				FCEU_printf(">> chr[%d] bank %d switched\n", i, chr_bank[sum]);
#endif
			} else
#ifdef MESSAGE_LOG
				FCEU_printf(">> chr[%d] bank %d switched the same\n", i, chr_bank[sum]);
		}
#endif
				}
				for (i = 0; i < 4; i++) {
					int32 sum = cmds.states[cmd.found].prgsum[i];
					if (sum != -1) {
						if (sum != state_cur.prgsum[i]) {
							state_cur.prgsum[i] = sum;
							setprg8r(2, 0x8000 + (i * 8192), PRGCUR(i));
#ifdef MESSAGE_LOG
							FCEU_printf(">> prg[%d] bank %d switched\n", i, prg_bank[sum]);
#endif
						} else
#ifdef MESSAGE_LOG
							FCEU_printf(">> prg[%d] bank %d switched the same\n", i, prg_bank[sum]);
					}
#endif
							}
							}

							static void LogCmd() {
								int32 i;
								FCEU_printf(">> new cmd size %d [", cmd_cache[cmd.hashf].size);
								for (i = 0; i < cmd_cache[cmd.hashf].size; i++)
									FCEU_printf(" %06X", cmds.seqs[cmd.found][i]);
								FCEU_printf(" ], switched to (");
								if (cmds.states[cmd.found].mirror != -1)
									FCEU_printf(" mirror=%s", mirror_names[mirror_modes[cmds.states[cmd.found].mirror]]);
								for (i = 0; i < 8; i++)
									if (cmds.states[cmd.found].chrsum[i] != -1)
										FCEU_printf(" chr%d=%02X", i, chr_bank[cmds.states[cmd.found].chrsum[i]]);
								for (i = 0; i < 4; i++)
									if (cmds.states[cmd.found].prgsum[i] != -1)
										FCEU_printf(" prg%d=%02X", i, prg_bank[cmds.states[cmd.found].prgsum[i]]);
								FCEU_printf(" )\n");
							}
#endif
static void Sync() {
	setchr1r(1, 0x0000, CHRCUR(0));
	setchr1r(1, 0x0400, CHRCUR(1));
	setchr1r(1, 0x0800, CHRCUR(2));
	setchr1r(1, 0x0C00, CHRCUR(3));
	setchr1r(1, 0x1000, CHRCUR(4));
	setchr1r(1, 0x1400, CHRCUR(5));
	setchr1r(1, 0x1800, CHRCUR(6));
	setchr1r(1, 0x1C00, CHRCUR(7));
#ifndef NO_RAM
	setprg8r(1, 0x6000, 0);
#endif
	setprg8r(2, 0x8000, PRGCUR(0));
	setprg8r(2, 0xA000, PRGCUR(1));
	setprg8r(2, 0xC000, PRGCUR(2));
	setprg8r(2, 0xE000, PRGCUR(3));
	setmirror(mirror_modes[state_cur.mirror]);
}
#ifndef NO_CACHE
static void UpdateCmd(uint32 val) {
	int32 index;
	if (cmd.size < CMD_MAX_SIZE) {
		index = cmd.size++;
	} else {
		/* если достигнут максимум для команды, выбросить последнуюю, добавить новую,
		 * продолжать до банксвичинга
		 */
		cmd.hash = 0;
		for (index = 0; index < (CMD_MAX_SIZE - 1); index++) {
			cmd.seq[index] = cmd.seq[index + 1];
			cmd.hash *= FNV_32_PRIME;
			cmd.hash ^= cmd.seq[index];
		}
	}
	cmd.seq[index] = val;
	cmd.hash *= FNV_32_PRIME;
	cmd.hash ^= val;
	cmd.hashf = (cmd.hash >> 16) ^ (cmd.hash & 0xffff);
	cmd.found = cmd_cache[cmd.hashf].index;
}
#endif

static DECLFW(MCopyFamiWrite) {
#ifndef NO_CACHE
	int32 i;
#endif

#ifdef MESSAGE_LOG
	FCEU_printf("> WRITE %04X:%02X\n", A, V);
#endif

	PRGWBCmd[1] = A & 0xFF;
	PRGWBCmd[2] = A >> 8;
	PRGWBCmd[3] = V & 0xFF;
	SEND(PRGWBCmd);
#ifdef NO_CACHE
	CheckStatus();
	Sync();
#else
	UpdateCmd((A << 8) | V);
	/* ищем команду в кеше */
	if (cmd.found == -1) {
		/* не найдена, проверяем, изменилось ли состояние банков
		 * либо не предельной ли она длины для команды
		 */
		cmd_cache[cmd.hashf].index = cmd.found = cmds.count++;
		cmd_cache[cmd.hashf].retest = 0;
		cmd_cache[cmd.hashf].verify = 0;
		for (i = 0; i < cmd.size; i++)
			cmds.seqs[cmd.found][i] = cmd.seq[i];
		cmd_cache[cmd.hashf].size = cmd.size;
		if (CheckStatus()) {
			cmds.states[cmd.found] = state_new;
			LogCmd();
			cmd.size = 0;
			cmd.hash = 0;
			Sync();
		} else {
			/* если добавлена полная команда без банксвитчинга */
			cmd_cache[cmd.hashf].index = -2;
		}
	} else if (cmd.found == -2) {
		/* частичное совпадение, если число проверок не превысило лимит */
		if (cmd_cache[cmd.hashf].retest < CMD_MAX_RETEST) {
			/* то проверим состояние банков */
			if (CheckStatus()) {
				/* изменилось, запишем новую команду */
				cmd_cache[cmd.hashf].index = cmd.found = cmds.count++;
				cmd_cache[cmd.hashf].retest = 0;
				cmd_cache[cmd.hashf].verify = 0;
				for (i = 0; i < cmd.size; i++)
					cmds.seqs[cmd.found][i] = cmd.seq[i];
				cmd_cache[cmd.hashf].size = cmd.size;
				cmds.states[cmd.found] = state_new;
				LogCmd();
				cmd.size = 0;
				cmd.hash = 0;
				Sync();
			} else {
				/* не изменилось, отметим увеличим счетчик проверок */
				cmd_cache[cmd.hashf].retest++;
			}
		}
	} else {
		/* найдена, последний рубеж обороны от говна */
#if 0
		if(cmd_cache[cmd.hashf].verify < CMD_MAX_VERIFY) {
			if(CheckStatus()) {
				int32 changed = 0;
				if(cmds.states[cmd.found].mirror != state_new.mirror)
					changed = 1;
				for(i=0; i<8; i++)
					if(cmds.states[cmd.found].chrsum[i] != state_new.chrsum[i])
						changed = 1;
				for(i=0; i<4; i++)
					if(cmds.states[cmd.found].prgsum[i] != state_new.prgsum[i])
						changed = 1;
				if(changed) {
					cmd_cache[cmd.hashf].index = -1;
					cmd_cache[cmd.hashf].retest = 0;
					cmd_cache[cmd.hashf].verify = 0;
					Sync();
				}
			} else
				cmd_cache[cmd.hashf].verify++;
		} else
#endif
		{
			/* применяем без малейшего зазрения совести */
			ApplyStatus();
			cmd.size = 0;
			cmd.hash = 0;
		}
	}
#endif
}

static DECLFR(MCopyFamiRead) {
	uint8 result;
	PRGRBCmd[1] = A & 0xFF;
	PRGRBCmd[2] = A >> 8;
	SENDGET(PRGRBCmd, result, 1);
#ifdef MESSAGE_LOG
	FCEU_printf("> READ %04X:%02X\n", A, result);
#endif
	return result;
}

static void MCopyFamiReset(void) {
	state_cur = state_def;
	Sync();
}

static void MCopyFamiPower(void) {
/*  uint32 resp, presp; */

	FCEU_printf("NOW POWERING... ");

	Sync();

	SetWriteHandler(0x4018, 0x7fff, MCopyFamiWrite);
	SetReadHandler(0x4018, 0x7fff, MCopyFamiRead);
#ifndef NO_RAM
	SetWriteHandler(0x6000, 0x7fff, CartBW);
	SetReadHandler(0x6000, 0x7fff, CartBR);
#endif

#if 0
	FCEU_printf("READING MEMORY MAP...\n");
	CPUTestCmd[1] = 0x50;
	CPUTestCmd[2] = 0x30;
	SEND(CPUTestCmd);
	resp = 0;
	presp = 0xffffffff;
	while (presp != 0x00ff0000) {
		GET(resp, 3);
		if(presp != 0xffffffff) {
			switch(presp & 0x00FF0000) {
				case 0x00000000: /* BUS */
					FCEU_printf("  %04X-%04X OPEN BUS\n",presp & 0x7fff, (resp - 1) & 0x7fff);
					break;
				case 0x00010000: /* RAM */
					FCEU_printf("  %04X-%04X RAM\n",presp & 0x7fff, (resp - 1) & 0x7fff);
					SetWriteHandler(presp & 0x7fff, (resp - 1) & 0x7fff, CartBW);
					SetReadHandler(presp & 0x7fff, (resp - 1) & 0x7fff, CartBR);
					break;
			}
		}
		presp = resp;
	}
#endif
	SetWriteHandler(0x8000, 0xffff, MCopyFamiWrite);
	SetReadHandler(0x8000, 0xffff, CartBR);

	FCEU_printf("DONE!\nNOW COLLECTING DATA...\n");
}

static void MCopyFamiClose(void) {
	if (chr_data.buf)
		FCEU_gfree(chr_data.buf);
	chr_data.buf = NULL;
	if (prg_data.buf)
		FCEU_gfree(prg_data.buf);
	prg_data.buf = NULL;
	if (WRAM)
		FCEU_gfree(WRAM);
	WRAM = NULL;

	SerialClose();
}

static void StateRestore(int version) {
	Sync();
}

void MapperCopyFami_Init(CartInfo *info) {
	uint32 resp = 0, i, size;

	memset(chr_bank, -1, sizeof(chr_bank));
	memset(prg_bank, -1, sizeof(chr_bank));
	memset(cmd_cache, -1, sizeof(cmd_cache));
	memset(&cmds, 0, sizeof(cmds));
	memset(&cmd, 0, sizeof(cmd));

	info->Reset = MCopyFamiReset;
	info->Power = MCopyFamiPower;
	info->Close = MCopyFamiClose;
	GameStateRestore = StateRestore;

	size = 1024 * CHR_CACHE_SIZE;	/* размер страницы 1кб */
	chr_data.buf = (uint8*)FCEU_gmalloc(size);
	SetupCartCHRMapping(1, chr_data.buf, size, 1);	/* проверять ппу рам, иначе игра может портить данные */
	AddExState(chr_data.buf, size, 0, "CCHR");

	size = 8192;	/* размер страницы 8кб */
	WRAM = (uint8*)FCEU_gmalloc(size);
	SetupCartPRGMapping(1, WRAM, size, 1);
	AddExState(WRAM, size, 0, "CPRM");

	size = 8192 * PRG_CACHE_SIZE;	/* размер страницы 8кб */
	prg_data.buf = (uint8*)FCEU_gmalloc(size);
	SetupCartPRGMapping(2, prg_data.buf, size, 0);
	AddExState(prg_data.buf, size, 0, "CPPR");


	FCEU_printf("WAITING FOR SERIAL PORT... ");
	while (!SerialOpen(19, 921600)) {
		Sleep(500);
	}
	FCEU_printf("READY!\n");

	FCEU_printf("WAITING FOR DEVICE... ");

	while (resp != *(uint32*)&InitVector[0]) {
		SEND(ResetCmd);
		SENDGET(InitVector, resp, 4);
		Sleep(500);
	}

	FCEU_printf("READY!\n");
	FCEU_printf("READING STATUS...\n");
	GetStatus(&state_cur);
	FCEU_printf("MIRRORING IS %s (%02X)\n", mirror_names[mirror_modes[state_cur.mirror]], state_cur.mirror);
	FCEU_printf("READING CHR...\n INITIAL STATE:");

	for (i = 0; i < 8; i++) {
		if (CHRCUR(i) == -1)
			CHRCUR(i) = FetchNewCHRBank(i);
		FCEU_printf(" CHR%d=%02X", i, CHRCUR(i));
	}
	FCEU_printf("\n");

	FCEU_printf("READING PRG...\n INITIAL STATE:");
	for (i = 0; i < 4; i++) {
		if (PRGCUR(i) == -1)
			PRGCUR(i) = FetchNewPRGBank(i);
		FCEU_printf(" PRG%d=%02X", i, PRGCUR(i));
	}
	FCEU_printf("\nDONE!\n");

	state_def = state_cur;

	AddExState(&StateRegs, ~0, 0, 0);
}

#endif
