/*
** Nofrendo (c) 1998-2000 Matthew Conte (matt@conte.com)
**
**
** This program is free software; you can redistribute it and/or
** modify it under the terms of version 2 of the GNU Library General 
** Public License as published by the Free Software Foundation.
**
** 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 
** Library General Public License for more details.  To obtain a 
** copy of the GNU Library General Public License, write to the Free 
** Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
**
** Any permitted reproduction of these routines, in whole or in part,
** must bear this legend.
**
**
** nes_rom.c
**
** NES ROM loading/saving related functions
** $Id: nes_rom.c,v 1.2 2001/04/27 14:37:11 neil Exp $
*/

/* TODO: make this a generic ROM loading routine */

#include <stdio.h>
#include <string.h>
#include "noftypes.h"
#include "nes_rom.h"
#include "nes_mmc.h"
#include "nes_ppu.h"
#include "emuapi.h"
#include "nes.h"
#include "log.h"
#include "osd.h"

extern char *osd_getromdata();

/* Max length for displayed filename */
#define  ROM_DISP_MAXLEN   20


#define  ROM_FOURSCREEN    0x08
#define  ROM_TRAINER       0x04
#define  ROM_BATTERY       0x02
#define  ROM_MIRRORTYPE    0x01
#define  ROM_INES_MAGIC    "NES\x1A"

//ToDo: packed - JD
typedef struct inesheader_s
{
   uint8 ines_magic[4]    ;
   uint8 rom_banks        ;
   uint8 vrom_banks       ;
   uint8 rom_type         ;
   uint8 mapper_hinybble  ;
   uint8 reserved[8]      ;
} inesheader_t;


#define  TRAINER_OFFSET    0x1000
#define  TRAINER_LENGTH    0x200
#define  VRAM_LENGTH       0x2000

#define  ROM_BANK_LENGTH   0x4000
#define  VROM_BANK_LENGTH  0x2000

#define  SRAM_BANK_LENGTH  0x0400
#define  VRAM_BANK_LENGTH  0x2000


/* Save battery-backed RAM */
static void rom_savesram(rominfo_t *rominfo)
{
  // TODO - add battery backup support!

  emu_printf("SRAM save not yet supported");
  /*
   FILE *fp;
   char fn[PATH_MAX + 1];

   ASSERT(rominfo);

   if (rominfo->flags & ROM_FLAG_BATTERY)
   {
      strncpy(fn, rominfo->filename, PATH_MAX);
      osd_newextension(fn, ".sav");

      fp = fopen(fn, "wb");
      if (NULL != fp)
      {
         fwrite(rominfo->sram, SRAM_BANK_LENGTH, rominfo->sram_banks, fp);
         fclose(fp);
         log_printf("Wrote battery RAM to %s.\n", fn);
      }
   }
  */
}

/* Load battery-backed RAM from disk */
static void rom_loadsram(rominfo_t *rominfo)
{
  // TODO - add battery backup support!
  emu_printf("SRAM load not yet supported\n");

  /*
   FILE *fp;
   char fn[PATH_MAX + 1];

   ASSERT(rominfo);

   if (rominfo->flags & ROM_FLAG_BATTERY)
   {
      strncpy(fn, rominfo->filename, PATH_MAX);
      osd_newextension(fn, ".sav");

      fp = fopen(fn, "rb");
      if (NULL != fp)
      {
         fread(rominfo->sram, SRAM_BANK_LENGTH, rominfo->sram_banks, fp);
         fclose(fp);
         log_printf("Read battery RAM from %s.\n", fn);
      }
   }
  */
}

/* Allocate space for SRAM */
static int rom_allocsram(rominfo_t *rominfo)
{
#ifdef USE_SRAM
   /* Load up SRAM */
   emu_printf("Alloc SRAM...");
   rominfo->sram = emu_Malloc(SRAM_BANK_LENGTH * rominfo->sram_banks);
   if (NULL == rominfo->sram)
   {
      emu_Halt("Could not allocate space for battery RAM");
   }

   /* make damn sure SRAM is clear */
   memset(rominfo->sram, 0, SRAM_BANK_LENGTH * rominfo->sram_banks);
#else
   rominfo->sram = NULL;
#endif
   return 0;
}

/* If there's a trainer, load it in at $7000 */
static void rom_loadtrainer(unsigned char **rom, rominfo_t *rominfo)
{
   ASSERT(rom);
   ASSERT(rominfo);

   if (rominfo->flags & ROM_FLAG_TRAINER)
   {
//      fread(rominfo->sram + TRAINER_OFFSET, TRAINER_LENGTH, 1, fp);
      memcpy(rominfo->sram + TRAINER_OFFSET, *rom, TRAINER_LENGTH);
      rom+=TRAINER_LENGTH;
      emu_printf("Read in trainer at $7000\n");
   }
}




static int rom_loadrom(unsigned char **rom, rominfo_t *rominfo)
{
   ASSERT(rom);
   ASSERT(rominfo);
   /* Allocate ROM space, and load it up! */
   rominfo->rom=*rom;
   *rom+=ROM_BANK_LENGTH*rominfo->rom_banks;

   /* If there's VROM, allocate and stuff it in */
   if (rominfo->vrom_banks)
   {
      rominfo->vrom=*rom;
      *rom+=VROM_BANK_LENGTH*rominfo->vrom_banks;
   }
   else
   {
      emu_printf("Alloc VRAM");
      rominfo->vram = emu_Malloc(VRAM_LENGTH);
      if (NULL == rominfo->vram)
      {
         return -1;
      }
      memset(rominfo->vram, 0, VRAM_LENGTH);
   }
   return 0;
}


static int rom_findrom(const char *filename, rominfo_t *rominfo)
{
   int fp;

   ASSERT(rominfo);

   if (NULL == filename)
      return 0;

   /* Make a copy of the name so we can extend it */
   osd_fullname(rominfo->filename, filename);

   fp = emu_FileOpen(rominfo->filename);
   if (!fp)
   {
      /* Didn't find the file?  Maybe the .NES extension was omitted */
      if (NULL == strrchr(rominfo->filename, '.'))
         strncat(rominfo->filename, ".nes", PATH_MAX - strlen(rominfo->filename));

      /* this will either return NULL or a valid file pointer */
      fp = emu_FileOpen(rominfo->filename);
   }

   return fp;
}

/* Add ROM name to a list with dirty headers */
static int rom_adddirty(char *filename)
{
   return 0;
}

/* return 0 if this *is* an iNES file */
int rom_checkmagic(const char *filename)
{
   inesheader_t head;
   rominfo_t rominfo;
   int fp;

   fp = (int)rom_findrom(filename, &rominfo);
   if (0 == fp)
      return -1;

   emu_FileRead((uint8_t *)&head, 1*sizeof(head));

   emu_FileClose();

   if (0 == memcmp(head.ines_magic, ROM_INES_MAGIC, 4))
      /* not an iNES file */
      return 0;

   return -1;
}



static int rom_getheader(unsigned char **rom, rominfo_t *rominfo)
{
#define  RESERVED_LENGTH   8
   inesheader_t head;
   uint8 reserved[RESERVED_LENGTH];
   bool header_dirty;

   ASSERT(rom);
   ASSERT(*rom);
   ASSERT(rominfo);

   /* Read in the header */
//   _fread(&head, 1, sizeof(head), fp);
	log_printf("Head: %p (%x %x %x %x)\n", *rom, (*rom)[0], (*rom)[1], (*rom)[2], (*rom)[3]);
	memcpy(&head, *rom, sizeof(head));
	*rom+=sizeof(head);

   if (memcmp(head.ines_magic, ROM_INES_MAGIC, 4))
   {
     emu_Halt("Not a valid ROM image");
      return -1;
   }

   rominfo->rom_banks = head.rom_banks;
   rominfo->vrom_banks = head.vrom_banks;
   /* iNES assumptions */
   rominfo->sram_banks = 8; /* 1kB banks, so 8KB */
   rominfo->vram_banks = 1; /* 8kB banks, so 8KB */
   rominfo->mirror = (head.rom_type & ROM_MIRRORTYPE) ? MIRROR_VERT : MIRROR_HORIZ;
   rominfo->flags = 0;
   if (head.rom_type & ROM_BATTERY)
      rominfo->flags |= ROM_FLAG_BATTERY;
   if (head.rom_type & ROM_TRAINER)
      rominfo->flags |= ROM_FLAG_TRAINER;
   if (head.rom_type & ROM_FOURSCREEN)
      rominfo->flags |= ROM_FLAG_FOURSCREEN;
   /* TODO: fourscreen a mirroring type? */
   rominfo->mapper_number = head.rom_type >> 4;

   /* Do a compare - see if we've got a clean extended header */
   memset(reserved, 0, RESERVED_LENGTH);
   if (0 == memcmp(head.reserved, reserved, RESERVED_LENGTH))
   {
      /* We were clean */
      header_dirty = false;
      rominfo->mapper_number |= (head.mapper_hinybble & 0xF0);
   }
   else
   {
      header_dirty = true;

      /* @!?#@! DiskDude. */
      if (('D' == head.mapper_hinybble) && (0 == memcmp(head.reserved, "iskDude!", 8)))
         log_printf("`DiskDude!' found in ROM header, ignoring high mapper nybble\n");
      else
      {
         log_printf("ROM header dirty, possible problem\n");
         rominfo->mapper_number |= (head.mapper_hinybble & 0xF0);
      }

      rom_adddirty(rominfo->filename);
   }

   /* TODO: this is an ugly hack, but necessary, I guess */
   /* Check for VS unisystem mapper */
   if (99 == rominfo->mapper_number)
      rominfo->flags |= ROM_FLAG_VERSUS;

   return 0;
}

/* Build the info string for ROM display */
char *rom_getinfo(rominfo_t *rominfo)
{
   static char info[PATH_MAX + 1];
   char romname[PATH_MAX + 1], temp[PATH_MAX + 1];

   /* Look to see if we were given a path along with filename */
   /* TODO: strip extensions */
   if (strrchr(rominfo->filename, PATH_SEP))
      strncpy(romname, strrchr(rominfo->filename, PATH_SEP) + 1, PATH_MAX);
   else
      strncpy(romname, rominfo->filename, PATH_MAX);

   /* If our filename is too long, truncate our displayed filename */
   if (strlen(romname) > ROM_DISP_MAXLEN)
   {
      strncpy(info, romname, ROM_DISP_MAXLEN - 3);
      strcpy(info + (ROM_DISP_MAXLEN - 3), "...");
   }
   else
   {
      strcpy(info, romname);
   }

   sprintf(temp, " [%d] %dk/%dk %c", rominfo->mapper_number,
           rominfo->rom_banks * 16, rominfo->vrom_banks * 8,
           (rominfo->mirror == MIRROR_VERT) ? 'V' : 'H');
   
   /* Stick it on there! */
   strncat(info, temp, PATH_MAX - strlen(info));

   if (rominfo->flags & ROM_FLAG_BATTERY)
      strncat(info, "B", PATH_MAX - strlen(info));
   if (rominfo->flags & ROM_FLAG_TRAINER)
      strncat(info, "T", PATH_MAX - strlen(info));
   if (rominfo->flags & ROM_FLAG_FOURSCREEN)
      strncat(info, "4", PATH_MAX - strlen(info));

   return info;
}

/* Load a ROM image into memory */
rominfo_t *rom_load(const char *filename)
{
   unsigned char *rom=(unsigned char*)osd_getromdata();
   rominfo_t *rominfo;

   emu_printf("Alloc ROMinfo...");
   rominfo = emu_Malloc(sizeof(rominfo_t));
   if (NULL == rominfo)
      return NULL;

   memset(rominfo, 0, sizeof(rominfo_t));
   strncpy(rominfo->filename, filename, sizeof(rominfo->filename));
   emu_printf("rom_load: rominfo->filename\n");

   /* Get the header and stick it into rominfo struct */
   if (rom_getheader(&rom, rominfo))
      goto _fail;

   /* Make sure we really support the mapper */
   if (false == mmc_peek(rominfo->mapper_number))
   {
      emu_Halt("Mapper not yet implemented");
      goto _fail;
   }


   /* iNES format doesn't tell us if we need SRAM, so
   ** we have to always allocate it -- bleh!
   ** UNIF, TAKE ME AWAY!  AAAAAAAAAA!!!
   */
   if (rom_allocsram(rominfo))
      goto _fail;

   rom_loadtrainer(&rom, rominfo);

   if (rom_loadrom(&rom, rominfo))
      goto _fail;

   rom_loadsram(rominfo);

   emu_printf("ROM loaded\n");

   return rominfo;

_fail:
   rom_free(&rominfo);
   return NULL;
}

/* Free a ROM */
void rom_free(rominfo_t **rominfo)
{
   if (NULL == *rominfo)
   {
      emu_printf("ROM isnt loaded\n");
      return;
   }

   /* Restore palette if we loaded in a VS jobber */
   if ((*rominfo)->flags & ROM_FLAG_VERSUS)
   {
      /* TODO: bad idea calling nes_getcontextptr... */
      ppu_setdefaultpal(nes_getcontextptr()->ppu);
      log_printf("Default NES palette restored\n");
   }

   rom_savesram(*rominfo);

   if ((*rominfo)->sram)
      emu_Free((*rominfo)->sram);
   if ((*rominfo)->rom)
      emu_Free((*rominfo)->rom);
   if ((*rominfo)->vrom)
      emu_Free((*rominfo)->vrom);
   if ((*rominfo)->vram)
      emu_Free((*rominfo)->vram);

   emu_Free(*rominfo);
   emu_printf("ROM freed");
}

/*
** $Log: nes_rom.c,v $
** Revision 1.2  2001/04/27 14:37:11  neil
** wheeee
**
** Revision 1.1.1.1  2001/04/27 07:03:54  neil
** initial
**
** Revision 1.8  2000/11/21 13:28:40  matt
** take care to zero allocated mem
**
** Revision 1.7  2000/11/09 14:07:28  matt
** state load fixed, state save mostly fixed
**
** Revision 1.6  2000/10/28 14:24:54  matt
** where did I put that underscore?
**
** Revision 1.5  2000/10/27 12:56:35  matt
** api change for ppu palette functions
**
** Revision 1.4  2000/10/26 22:51:44  matt
** correct NULL filename handling
**
** Revision 1.3  2000/10/25 01:23:08  matt
** basic system autodetection
**
** Revision 1.2  2000/10/25 00:23:16  matt
** makefiles updated for new directory structure
**
** Revision 1.1  2000/10/24 12:20:28  matt
** changed directory structure
**
** Revision 1.19  2000/10/21 14:35:58  matt
** typo
**
** Revision 1.18  2000/10/17 03:22:37  matt
** cleaning up rom module
**
** Revision 1.17  2000/10/10 13:58:13  matt
** stroustrup squeezing his way in the door
**
** Revision 1.16  2000/10/10 13:03:54  matt
** Mr. Clean makes a guest appearance
**
** Revision 1.15  2000/07/31 04:28:46  matt
** one million cleanups
**
** Revision 1.14  2000/07/30 04:31:26  matt
** automagic loading of the nofrendo intro
**
** Revision 1.13  2000/07/25 02:20:58  matt
** cleanups
**
** Revision 1.12  2000/07/20 01:53:27  matt
** snprintf() ain't no standard function, eh?
**
** Revision 1.11  2000/07/19 16:06:54  neil
** little error fixed (tempinfo vs rominfo->info)
**
** Revision 1.10  2000/07/19 15:59:39  neil
** PATH_MAX, strncpy, snprintf, and strncat are our friends
**
** Revision 1.9  2000/07/17 01:52:27  matt
** made sure last line of all source files is a newline
**
** Revision 1.8  2000/07/06 16:47:50  matt
** new ppu palette setting calls
**
** Revision 1.7  2000/07/05 23:21:54  neil
** fclose(fp) should not be done if fp == NULL
**
** Revision 1.6  2000/07/04 04:45:14  matt
** changed include
**
** Revision 1.5  2000/06/26 04:56:10  matt
** minor cleanup
**
** Revision 1.4  2000/06/09 15:12:25  matt
** initial revision
**
*/
