#include "driver.h"
#include "cpu/m6502/m6502.h"
#include "video/generic.h"
#include "video/ppu2c0x.h"
#include "includes/nes.h"
#include "machine/nes_mmc.h"
#include "sound/nes_apu.h"
#include "zlib.h"
#include "image.h"
#include "hash.h"



/***************************************************************************
    CONSTANTS
***************************************************************************/

/* Set to dump info about the inputs to the errorlog */
#define LOG_JOY		0

/* Set to generate prg chunk files when the cart is loaded */
#define SPLIT_PRG	0

#define BATTERY_SIZE 0x2000



/***************************************************************************
    TYPE DEFINITIONS
***************************************************************************/

typedef struct _nes_input nes_input;
struct _nes_input
{
	UINT32 shift;
	UINT32 i0, i1, i2;
};



/***************************************************************************
    GLOBAL VARIABLES
***************************************************************************/

unsigned char *battery_ram;
static UINT8 battery_data[BATTERY_SIZE];

struct nes_struct nes;
struct fds_struct nes_fds;

static nes_input in_0;
static nes_input in_1;



/***************************************************************************
    FUNCTION PROTOTYPES
***************************************************************************/

static void init_nes_core(void);
static void nes_machine_stop(running_machine *machine);



/***************************************************************************
    FUNCTIONS
***************************************************************************/

static mess_image *cartslot_image(void)
{
	return image_from_devtype_and_index(IO_CARTSLOT, 0);
}

static void init_nes_core (void)
{
	/* We set these here in case they weren't set in the cart loader */
	nes.rom = memory_region(REGION_CPU1);
	nes.vrom = memory_region(REGION_GFX1);
	nes.vram = memory_region(REGION_GFX2);
	nes.wram = memory_region(REGION_USER1);

	/* Brutal hack put in as a consequence of the new memory system; we really
	 * need to fix the NES code */
	memory_install_read8_handler(0, ADDRESS_SPACE_PROGRAM, 0x0000, 0x07ff, 0, 0x1800, MRA8_BANK10);
	memory_install_write8_handler(0, ADDRESS_SPACE_PROGRAM, 0x0000, 0x07ff, 0, 0x1800, MWA8_BANK10);
	memory_set_bankptr(10, nes.rom);

	battery_ram = nes.wram;

	/* Set up the memory handlers for the mapper */
	switch (nes.mapper)
	{
		case 20:
			nes.slow_banking = 0;
			memory_install_read8_handler(0, ADDRESS_SPACE_PROGRAM, 0x4030, 0x403f, 0, 0, fds_r);
			memory_install_read8_handler(0, ADDRESS_SPACE_PROGRAM, 0x6000, 0xdfff, 0, 0, MRA8_RAM);
			memory_install_read8_handler(0, ADDRESS_SPACE_PROGRAM, 0xe000, 0xffff, 0, 0, MRA8_BANK1);
			memory_set_bankptr(1, &nes.rom[0xe000]);

			memory_install_write8_handler(0, ADDRESS_SPACE_PROGRAM, 0x4020, 0x402f, 0, 0, fds_w);
			memory_install_write8_handler(0, ADDRESS_SPACE_PROGRAM, 0x6000, 0xdfff, 0, 0, MWA8_RAM);
			memory_install_write8_handler(0, ADDRESS_SPACE_PROGRAM, 0xe000, 0xffff, 0, 0, MWA8_ROM);
			break;
		case 40:
			nes.slow_banking = 1;
			/* Game runs code in between banks, so we do things different */
			memory_install_read8_handler(0, ADDRESS_SPACE_PROGRAM, 0x6000, 0x7fff, 0, 0, MRA8_RAM);
			memory_install_read8_handler(0, ADDRESS_SPACE_PROGRAM, 0x8000, 0xffff, 0, 0, MRA8_ROM);

			memory_install_write8_handler(0, ADDRESS_SPACE_PROGRAM, 0x6000, 0x7fff, 0, 0, nes_mid_mapper_w);
			memory_install_write8_handler(0, ADDRESS_SPACE_PROGRAM, 0x8000, 0xffff, 0, 0, nes_mapper_w);
			break;
		default:
			nes.slow_banking = 0;
			memory_install_read8_handler(0, ADDRESS_SPACE_PROGRAM, 0x6000, 0x7fff, 0, 0, MRA8_BANK5);
			memory_install_read8_handler(0, ADDRESS_SPACE_PROGRAM, 0x8000, 0x9fff, 0, 0, MRA8_BANK1);
			memory_install_read8_handler(0, ADDRESS_SPACE_PROGRAM, 0xa000, 0xbfff, 0, 0, MRA8_BANK2);
			memory_install_read8_handler(0, ADDRESS_SPACE_PROGRAM, 0xc000, 0xdfff, 0, 0, MRA8_BANK3);
			memory_install_read8_handler(0, ADDRESS_SPACE_PROGRAM, 0xe000, 0xffff, 0, 0, MRA8_BANK4);

			memory_install_write8_handler(0, ADDRESS_SPACE_PROGRAM, 0x6000, 0x7fff, 0, 0, nes_mid_mapper_w);
			memory_install_write8_handler(0, ADDRESS_SPACE_PROGRAM, 0x8000, 0xffff, 0, 0, nes_mapper_w);
			break;
	}

	/* Set up the mapper callbacks */
	{
		const mmc *mapper;

		mapper = nes_mapper_lookup(nes.mapper);
		if (mapper)
		{
			mmc_write_low = mapper->mmc_write_low;
			mmc_read_low = mapper->mmc_read_low;
			mmc_write_mid = mapper->mmc_write_mid;
			mmc_write = mapper->mmc_write;
			ppu_latch = mapper->ppu_latch;
//			mmc_irq = mmc_list[i].mmc_irq;
		}
		else
		{
			logerror ("Mapper %d is not yet supported, defaulting to no mapper.\n",nes.mapper);
			mmc_write_low = mmc_write_mid = mmc_write = NULL;
			mmc_read_low = NULL;
		}
	}

	/* Load a battery file, but only if there's no trainer since they share */
	/* overlapping memory. */
	if (nes.trainer) return;

	/* We need this because battery ram is loaded before the */
	/* memory subsystem is set up. When this routine is called */
	/* everything is ready, so we can just copy over the data */
	/* we loaded before. */
	memcpy (battery_ram, battery_data, BATTERY_SIZE);
}

int nes_ppu_vidaccess( int num, int address, int data )
{
	/* TODO: this is a bit of a hack, needed to get Argus, ASO, etc to work */
	/* but, B-Wings, submath (j) seem to use this location differently... */
	if (nes.chr_chunks && (address & 0x3fff) < 0x2000)
	{
//		int vrom_loc;
//		vrom_loc = (nes_vram[(address & 0x1fff) >> 10] * 16) + (address & 0x3ff);
//		data = nes.vrom [vrom_loc];
	}
	return data;
}

static void nes_machine_reset(running_machine *machine)
{
	/* Some carts have extra RAM and require it on at startup, e.g. Metroid */
	nes.mid_ram_enable = 1;

	/* Reset the mapper variables. Will also mark the char-gen ram as dirty */
	mapper_reset (nes.mapper);

	/* Reset the serial input ports */
	in_0.shift = 0;
	in_1.shift = 0;
}

MACHINE_START( nes )
{
	init_nes_core();
	add_reset_callback(machine, nes_machine_reset);
	add_exit_callback(machine, nes_machine_stop);

	if ((!image_exists(image_from_devtype_and_index(IO_CARTSLOT, 0))) && (!image_exists(image_from_devtype_and_index(IO_FLOPPY, 0))))
	{
		/* NPW 05-Mar-2006 - Hack to keep the Famicom from crashing */
		static const UINT8 infinite_loop[] = { 0x4C, 0xF9, 0xFF, 0xF9, 0xFF }; /* JMP $FFF9, DC.W $FFF9 */
		memory_install_read8_handler(0, ADDRESS_SPACE_PROGRAM, 0xFFF9, 0xFFFD, 0, 0, MRA8_BANK11);
		memory_install_write8_handler(0, ADDRESS_SPACE_PROGRAM, 0xFFF9, 0xFFFD, 0, 0, MWA8_BANK11);
		memory_set_bankptr(11, (void *) infinite_loop);
	}
}

static void nes_machine_stop(running_machine *machine)
{
	/* Write out the battery file if necessary */
	if (nes.battery)
		image_battery_save(cartslot_image(), battery_ram, BATTERY_SIZE);
}



static int zapper_hit_pixel(const nes_input *input)
{
	UINT16 pix = 0;
	rgb_t col;
	UINT8 r, g, b;
	extern mame_bitmap *nes_zapper_hack;

	if (nes_zapper_hack)
		pix = *BITMAP_ADDR16(nes_zapper_hack, input->i2, input->i1);

	col = palette_get_color(Machine, pix);
	r = (UINT8) (col >> 16);
	g = (UINT8) (col >> 8);
	b = (UINT8) (col >> 0);

	return (((UINT16) r) + ((UINT16) g) + ((UINT16) b)) >= 240*3;
}



 READ8_HANDLER ( nes_IN0_r )
{
	int cfg;
	int retVal;

	/* Some games expect bit 6 to be set because the last entry on the data bus shows up */
	/* in the unused upper 3 bits, so typically a read from $4016 leaves 0x40 there. */
	retVal = 0x40;

	retVal |= ((in_0.i0 >> in_0.shift) & 0x01);

	/* Check the configuration to see what's connected */
	cfg = readinputport(PORT_CONFIG1);

	if (((cfg & 0x000f) == 0x0002) || ((cfg & 0x000f) == 0x0003))
	{
		/* If button 1 is pressed, indicate the light gun trigger is pressed */
		retVal |= ((in_0.i0 & 0x01) << 4);

		/* Look at the screen and see if the cursor is over a bright pixel */
		if (zapper_hit_pixel(&in_0))
			retVal &= ~0x08; /* sprite hit */
		else
			retVal |= 0x08;  /* no sprite hit */
	}

	if (LOG_JOY)
		logerror ("joy 0 read, val: %02x, pc: %04x, bits read: %d, chan0: %08x\n", retVal, activecpu_get_pc(), in_0.shift, in_0.i0);

	in_0.shift ++;
	return retVal;
}

 READ8_HANDLER ( nes_IN1_r )
{
	int cfg;
	int retVal;

	/* Some games expect bit 6 to be set because the last entry on the data bus shows up */
	/* in the unused upper 3 bits, so typically a read from $4017 leaves 0x40 there. */
	retVal = 0x40;

	/* Handle data line 0's serial output */
	retVal |= ((in_1.i0 >> in_1.shift) & 0x01);

	/* Check the fake dip to see what's connected */
	cfg = readinputport(PORT_CONFIG1);

	if (((cfg & 0x00f0) == 0x0020) || ((cfg & 0x00f0) == 0x0030))
	{
		/* zapper */
		/* If button 1 is pressed, indicate the light gun trigger is pressed */
		retVal |= ((in_1.i0 & 0x01) << 4);

		/* Look at the screen and see if the cursor is over a bright pixel */
		if (zapper_hit_pixel(&in_1))
			retVal &= ~0x08; /* sprite hit */
		else
			retVal |= 0x08;  /* no sprite hit */
	}
	else if ((cfg & 0x00f0) == 0x0040)
	{
		/* arkanoid dial */
		/* Handle data line 2's serial output */
		retVal |= ((in_1.i2 >> in_1.shift) & 0x01) << 3;

		/* Handle data line 3's serial output - bits are reversed */
		/* NPW 27-Nov-2007 - there is no third subscript! commenting out */
		/* retVal |= ((in_1[3] >> in_1.shift) & 0x01) << 4; */
		/* retVal |= ((in_1[3] << in_1.shift) & 0x80) >> 3; */
	}

	if (LOG_JOY)
		logerror ("joy 1 read, val: %02x, pc: %04x, bits read: %d, chan0: %08x\n", retVal, activecpu_get_pc(), in_1.shift, in_1.i0);

	in_1.shift ++;
	return retVal;
}



static void nes_read_input_device(int cfg, nes_input *vals, int pad_port,
	int supports_zapper, int paddle_port)
{
	vals->i0 = 0;
	vals->i1 = 0;
	vals->i2 = 0;

	switch(cfg & 0x0f)
	{
		case 0x01:	/* gamepad */
			if (pad_port >= 0)
				vals->i0 = readinputport(pad_port);
			break;

		case 0x02:	/* zapper 1 */
			if (supports_zapper)
			{
				vals->i0 = readinputport(PORT_ZAPPER0_T);
				vals->i1 = readinputport(PORT_ZAPPER0_X);
				vals->i2 = readinputport(PORT_ZAPPER0_Y);
			}
			break;

		case 0x03:	/* zapper 2 */
			if (supports_zapper)
			{
				vals->i0 = readinputport(PORT_ZAPPER1_T);
				vals->i1 = readinputport(PORT_ZAPPER1_X);
				vals->i2 = readinputport(PORT_ZAPPER1_Y);
			}
			break;

		case 0x04:	/* arkanoid paddle */
			if (paddle_port >= 0)
				vals->i0 = (UINT8) ((UINT8) readinputport (paddle_port) + (UINT8)0x52) ^ 0xff;
			break;
	}
}



WRITE8_HANDLER ( nes_IN0_w )
{
	int cfg;
	nes_input in_2;
	nes_input in_3;

	if (data & 0x01) return;

	if (LOG_JOY)
		logerror ("joy 0 bits read: %d\n", in_0.shift);

	/* Toggling bit 0 high then low resets both controllers */
	in_0.shift = 0;
	in_1.shift = 0;

	/* Check the configuration to see what's connected */
	cfg = readinputport(PORT_CONFIG1);

	/* Read the input devices */
	nes_read_input_device(cfg >>  0, &in_0, PORT_PAD0,  TRUE, -1);
	nes_read_input_device(cfg >>  4, &in_1, PORT_PAD1,  TRUE, PORT_PADDLE1);
	nes_read_input_device(cfg >>  8, &in_2, PORT_PAD2, FALSE, -1);
	nes_read_input_device(cfg >> 12, &in_3, PORT_PAD3, FALSE, -1);

	if (cfg & 0x0f00)
		in_0.i0 |= (in_2.i0 << 8) | (0x08 << 16);
	if (cfg & 0xf000)
		in_1.i0 |= (in_3.i0 << 8) | (0x04 << 16);
}



WRITE8_HANDLER ( nes_IN1_w )
{
}



DEVICE_LOAD(nes_cart)
{
	const char *mapinfo;
	int mapint1=0,mapint2=0,mapint3=0,mapint4=0,goodcrcinfo = 0;
	char magic[4];
	char skank[8];
	int local_options = 0;
	char m;
	int i;

	/* Verify the file is in iNES format */
	memset(magic, '\0', sizeof(magic));
	image_fread(image, magic, 4);

	if ((magic[0] != 'N') ||
		(magic[1] != 'E') ||
		(magic[2] != 'S'))
		goto bad;

	mapinfo = image_extrainfo(image);
	if (mapinfo)
	{
		if (4 == sscanf(mapinfo,"%d %d %d %d",&mapint1,&mapint2,&mapint3,&mapint4))
		{
			nes.mapper = mapint1;
			local_options = mapint2;
			nes.prg_chunks = mapint3;
			nes.chr_chunks = mapint4;
			logerror("NES.CRC info: %d %d %d %d\n",mapint1,mapint2,mapint3,mapint4);
			goodcrcinfo = 1;
		} else
		{
			logerror("NES: [%s], Invalid mapinfo found\n",mapinfo);
		}
	} else
	{
		logerror("NES: No extrainfo found\n");
	}
	if (!goodcrcinfo)
	{
		// image_extrainfo() resets the file position back to start.
		// Let's skip past the magic header once again.
		image_fseek (image, 4, SEEK_SET);

		image_fread (image, &nes.prg_chunks, 1);
		image_fread (image, &nes.chr_chunks, 1);
		/* Read the first ROM option byte (offset 6) */
		image_fread (image, &m, 1);

		/* Interpret the iNES header flags */
		nes.mapper = (m & 0xf0) >> 4;
		local_options = m & 0x0f;


		/* Read the second ROM option byte (offset 7) */
		image_fread (image, &m, 1);

		/* Check for skanky headers */
		image_fread (image, &skank, 8);

		/* If the header has junk in the unused bytes, assume the extra mapper byte is also invalid */
		/* We only check the first 4 unused bytes for now */
		for (i = 0; i < 4; i ++)
		{
			logerror("%02x ", skank[i]);
			if (skank[i] != 0x00)
			{
				logerror("(skank: %d)", i);
//				m = 0;
			}
		}
		logerror("\n");

		nes.mapper = nes.mapper | (m & 0xf0);
	}

	nes.hard_mirroring = local_options & 0x01;
	nes.battery = local_options & 0x02;
	nes.trainer = local_options & 0x04;
	nes.four_screen_vram = local_options & 0x08;

	if (nes.battery) logerror("-- Battery found\n");
	if (nes.trainer) logerror("-- Trainer found\n");
	if (nes.four_screen_vram) logerror("-- 4-screen VRAM\n");

	/* Free the regions that were allocated by the ROM loader */
	free_memory_region (Machine, REGION_CPU1);
	free_memory_region (Machine, REGION_GFX1);

	/* Allocate them again with the proper size */
	new_memory_region(Machine, REGION_CPU1, 0x10000 + (nes.prg_chunks+1) * 0x4000,0);
	if (nes.chr_chunks)
		new_memory_region(Machine, REGION_GFX1, nes.chr_chunks * 0x2000,0);

	nes.rom = memory_region(REGION_CPU1);
	nes.vrom = memory_region(REGION_GFX1);
	nes.vram = memory_region(REGION_GFX2);
	nes.wram = memory_region(REGION_USER1);

	/* Position past the header */
	image_fseek (image, 16, SEEK_SET);

	/* Load the 0x200 byte trainer at 0x7000 if it exists */
	if (nes.trainer)
	{
		image_fread (image, &nes.wram[0x1000], 0x200);
	}

	/* Read in the program chunks */
	if (nes.prg_chunks == 1)
	{
		image_fread (image, &nes.rom[0x14000], 0x4000);
		/* Mirror this bank into $8000 */
		memcpy (&nes.rom[0x10000], &nes.rom [0x14000], 0x4000);
	}
	else
		image_fread (image, &nes.rom[0x10000], 0x4000 * nes.prg_chunks);

#if SPLIT_PRG
	{
		FILE *prgout;
		char outname[255];

		for (i = 0; i < nes.prg_chunks; i ++)
		{
			sprintf (outname, "%s.p%d", battery_name, i);
			prgout = fopen (outname, "wb");
			if (prgout)
			{
				fwrite (&nes.rom[0x10000 + 0x4000 * i], 1, 0x4000, prgout);
				fclose (prgout);
			}
		}
	}
#endif /* SPLIT_PRG */

	logerror("**\n");
	logerror("Mapper: %d\n", nes.mapper);
	logerror("PRG chunks: %02x, size: %06x\n", nes.prg_chunks, 0x4000*nes.prg_chunks);

	/* Read in any chr chunks */
	if (nes.chr_chunks > 0)
	{
		image_fread (image, nes.vrom, nes.chr_chunks * 0x2000);
		if (nes.mapper == 2)
			logerror("Warning: VROM has been found in VRAM-based mapper. Either the mapper is set wrong or the ROM image is incorrect.\n");
	}

	logerror("CHR chunks: %02x, size: %06x\n", nes.chr_chunks, 0x4000*nes.chr_chunks);
	logerror("**\n");

	/* Attempt to load a battery file for this ROM. If successful, we */
	/* must wait until later to move it to the system memory. */
	if (nes.battery)
		image_battery_load(image, battery_data, BATTERY_SIZE);

	return INIT_PASS;

bad:
	logerror("BAD section hit during LOAD ROM.\n");
	return INIT_FAIL;
}



void nes_partialhash(char *dest, const unsigned char *data,
	unsigned long length, unsigned int functions)
{
	if (length <= 16)
		return;
	hash_compute(dest, &data[16], length - 16, functions);
}



DEVICE_LOAD(nes_disk)
{
	unsigned char magic[4];

	/* See if it has a fucking redundant header on it */
	image_fread(image, magic, 4);
	if ((magic[0] == 'F') &&
		(magic[1] == 'D') &&
		(magic[2] == 'S'))
	{
		/* Skip past the fucking redundant header */
		image_fseek (image, 0x10, SEEK_SET);
	}
	else
		/* otherwise, point to the start of the image */
		image_fseek (image, 0, SEEK_SET);

	/* clear some of the cart variables we don't use */
	nes.trainer = 0;
	nes.battery = 0;
	nes.prg_chunks = nes.chr_chunks = 0;

	nes.mapper = 20;
	nes.four_screen_vram = 0;
	nes.hard_mirroring = 0;

	nes_fds.sides = 0;
	nes_fds.data = NULL;

	/* read in all the sides */
	while (!image_feof (image))
	{
		nes_fds.sides ++;
		nes_fds.data = image_realloc(image, nes_fds.data, nes_fds.sides * 65500);
		if (!nes_fds.data)
			return INIT_FAIL;
		image_fread (image, nes_fds.data + ((nes_fds.sides-1) * 65500), 65500);
	}

	logerror("Number of sides: %d\n", nes_fds.sides);

	return INIT_PASS;
}

DEVICE_UNLOAD(nes_disk)
{
	/* TODO: should write out changes here as well */
	nes_fds.data = NULL;
}

void ppu_mirror_custom (int page, int address)
{
	fatalerror("Unimplemented");
}

void ppu_mirror_custom_vrom (int page, int address)
{
	fatalerror("Unimplemented");
}
