/* Mednafen - Multi-system Emulator
 *
 * Copyright notice for this file:
 *  Copyright (C) 2003 Xodnizel
 *
 * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include        <string.h>
#include	<stdio.h>
#include	<stdlib.h>
#include	<stdarg.h>

#include	"nes.h"
#include	"x6502.h"
#include	"ppu/ppu.h"
#include	"ppu/palette.h"
#include	"sound.h"

#include	"cart.h"
#include	"nsf.h"
#include	"fds.h"
#include	"ines.h"
#include	"unif.h"
#include        "cheat.h"
#include	"input.h"
#include	"vsuni.h"

extern MDFNGI EmulatedNES;

uint64 timestampbase;

static NESGameType *GameInterface = NULL;

readfunc ARead[0x10000 + 0x100];
writefunc BWrite[0x10000 + 0x100];
static readfunc *AReadG;
static writefunc *BWriteG;
static int RWWrap=0;

static DECLFW(BNull)
{
// printf("Null Write: %04x %02x\n", A, V);
}

static DECLFR(ANull)
{
 //printf("Null Read: %04x\n", A);
 return(X.DB);
}

int AllocGenieRW(void)
{
 if(!(AReadG=(readfunc *)MDFN_malloc(0x8000*sizeof(readfunc))))
  return 0;
 if(!(BWriteG=(writefunc *)MDFN_malloc(0x8000*sizeof(writefunc))))
  return 0;
 RWWrap=1;
 return 1;
}

void FlushGenieRW(void)
{
 int32 x;

 if(RWWrap)
 {
  for(x=0;x<0x8000;x++)
  {
   ARead[x+0x8000]=AReadG[x];
   BWrite[x+0x8000]=BWriteG[x];
  }
  free(AReadG);
  free(BWriteG);
  AReadG=0;
  BWriteG=0;
  RWWrap=0;
 }
}

readfunc GetReadHandler(int32 a)
{
  if(a>=0x8000 && RWWrap)
   return AReadG[a-0x8000];
  else
   return ARead[a];
}

void SetReadHandler(int32 start, int32 end, readfunc func)
{
  int32 x;

  if(!func)
   func=ANull;

  if(RWWrap)
   for(x=end;x>=start;x--)
   {
    if(x>=0x8000)
     AReadG[x-0x8000]=func;
    else
     ARead[x]=func;
   }
  else

   for(x=end;x>=start;x--)
    ARead[x]=func;
}

writefunc GetWriteHandler(int32 a)
{
  if(RWWrap && a>=0x8000)
   return BWriteG[a-0x8000];
  else
   return BWrite[a];
}

void SetWriteHandler(int32 start, int32 end, writefunc func)
{
  int32 x;

  if(!func)
   func=BNull;

  if(RWWrap)
   for(x=end;x>=start;x--)
   {
    if(x>=0x8000)
     BWriteG[x-0x8000]=func;
    else
     BWrite[x]=func;
   }
  else
   for(x=end;x>=start;x--)
    BWrite[x]=func;
}

uint8 GameMemBlock[131072];
uint8 RAM[0x800];
uint8 PAL=0;

static DECLFW(BRAML)
{  
        RAM[A]=V;
}

static DECLFW(BRAMH)
{
        RAM[A&0x7FF]=V;
}

static DECLFR(ARAML)
{
        return RAM[A];
}

static DECLFR(ARAMH)
{
        return RAM[A&0x7FF];
}

// We need to look up the correct function in the ARead[] and BWrite[] tables
// for these overflow functions, because the RAM access handlers might be hooked
// for cheats or other things.

static DECLFR(AOverflow)
{
	A &= 0xFFFF;
	X.PC &= 0xFFFF;
	return(ARead[A](A));
}

static DECLFW(BOverflow)
{
	A &= 0xFFFF;
	return(BWrite[A](A, V));
}

static void CloseGame(void)
{
 if(MDFNGameInfo->nes.type!=GIT_NSF)
  MDFN_FlushGameCheats(0,0);
 if(GameInterface)
 {
  if(GameInterface->Close)
   GameInterface->Close();
  free(GameInterface);
  GameInterface = NULL;
 }
 CloseGenie();
 MDFNSND_Close();
}

int UNIFLoad(const char *name, MDFNFILE *fp, NESGameType *);
int iNESLoad(const char *name, MDFNFILE *fp, NESGameType *);
int FDSLoad(const char *name, MDFNFILE *fp, NESGameType *);
int NSFLoad(const char *name, MDFNFILE *fp, NESGameType *);

static void ResetVidSys(void);

static int Load(const char *name, MDFNFILE *fp)
{
	int (*LoadFunctions[4])(const char *name, MDFNFILE *fp, NESGameType *) = { iNESLoad, UNIFLoad, NSFLoad, FDSLoad };
	int x;

        PPU_hook = 0;
        GameHBIRQHook = 0;
        if(GameExpSound.Kill)
         GameExpSound.Kill();
        memset(&GameExpSound,0,sizeof(GameExpSound));
        MapIRQHook = 0;
        MMC5Hack = 0;
        PAL &= 1;
        MDFN_SetPPUPalette(0);

        MDFNGameInfo->nes.type=GIT_CART;
        MDFNGameInfo->nes.vidsys=GIV_USER;
        MDFNGameInfo->nes.input[0]=MDFNGameInfo->nes.input[1]=-1;
        MDFNGameInfo->nes.inputfc=-1;
        MDFNGameInfo->nes.cspecial=0;
	MDFNGameInfo->soundchan = 1;

        if(MDFN_GetSettingB("nes.fnscan"))
	{
	 if(strstr(name, "(U)"))
	  MDFNGameInfo->nes.vidsys = GIV_NTSC;
         else if(strstr(name, "(J)"))
          MDFNGameInfo->nes.vidsys = GIV_NTSC;
         else if(strstr(name, "(E)") || strstr(name, "(G)"))
          MDFNGameInfo->nes.vidsys = GIV_PAL;
        }

	GameInterface = (NESGameType *)calloc(1, sizeof(NESGameType));

        SetReadHandler(0x0000,0xFFFF,ANull);
        SetWriteHandler(0x0000,0xFFFF,BNull);

        SetReadHandler(0x10000,0x10000 + 0xFF,AOverflow);
        SetWriteHandler(0x10000,0x10000 + 0xFF,BOverflow);

        SetReadHandler(0,0x7FF,ARAML);
        SetWriteHandler(0,0x7FF,BRAML);

        SetReadHandler(0x800,0x1FFF,ARAMH);  /* Part of a little */
        SetWriteHandler(0x800,0x1FFF,BRAMH); /* hack for a small speed boost. */

	for(x=0; x<4; x++)
	{
	 int t = LoadFunctions[x](name, fp, GameInterface);

	 if(t == 0)
         {
	  free(GameInterface);
	  GameInterface = NULL;
	  return(0);
	 }
	 else if(t == -1)
	 {
	  if(x == 3)
	  {
	   free(GameInterface);
	   GameInterface = NULL;
           return(-1);
	  }
	 }
	 else
	  break;	// File loaded successfully.
	}

        ResetVidSys();
	X6502_Init();
	MDFNPPU_Init();
        MDFNSND_Init(PAL);
	NESINPUT_Init();

        if(MDFNGameInfo->nes.type!=GIT_NSF)
         if(MDFN_GetSettingB("nes.gg"))
	  OpenGenie();

        PowerNES();

        if(MDFNGameInfo->nes.type!=GIT_NSF)
        {
         MDFN_LoadGamePalette();
         MDFN_LoadGameCheats(0);
        }
        MDFN_ResetPalette();
        return(1);
}

void MDFNNES_Kill(void)
{
 MDFN_KillGenie();
}

static int needrew = 0;

static void DoRewind(void)
{
 needrew = 1;
}

static void Emulate(uint32 *pXBuf, MDFN_Rect *LineWidths, float **SoundBuf, int32 *SoundBufSize, int skip)
{
 int r,ssize;
 int didrew;

 MDFNGameInfo->fb = pXBuf;

 MDFN_UpdateInput();

 if(MDFNGameInfo->nes.type!=GIT_NSF)
  didrew = MDFN_StateEvil(needrew); 
 else
  didrew = 0;

 if(geniestage!=1) MDFN_ApplyPeriodicCheats();
 r=MDFNPPU_Loop(skip);

 ssize=FlushEmulateSound(didrew);
 needrew = 0;

 timestampbase += timestamp;

 timestamp = 0;

 if(MDFNGameInfo->nes.type == GIT_NSF)
  MDFNNES_DrawNSF(pXBuf, WaveFinal, ssize);

 *SoundBuf=WaveFinal;
 *SoundBufSize=ssize;
}

void ResetNES(void)
{
        MDFNMOV_AddCommand(MDFNNPCMD_RESET);
	if(GameInterface->Reset)
         GameInterface->Reset();
        MDFNSND_Reset();
        MDFNPPU_Reset();
        X6502_Reset();
}

void MDFN_MemoryRand(uint8 *ptr, uint32 size)
{
 int x=0;
 while(size)
 {
  *ptr=(x&4)?0xFF:0x00;
  x++;
  size--;
  ptr++;
 }
}

void PowerNES(void) 
{
        MDFNMOV_AddCommand(MDFNNPCMD_POWER);
        if(!MDFNGameInfo) return;

	MDFN_CheatResetRAM();
	MDFN_CheatAddRAM(2,0,RAM);

        GeniePower();

	MDFN_MemoryRand(RAM,0x800);
	//memset(RAM,0xFF,0x800);

        NESINPUT_Power();
        MDFNSND_Power();
        MDFNPPU_Power();

	/* Have the external game hardware "powered" after the internal NES stuff.  
	   Needed for the NSF code and VS System code.
	*/
	if(GameInterface->Power)
	 GameInterface->Power();
        if(MDFNGameInfo->nes.type==GIT_VSUNI)
         MDFN_VSUniPower();


	timestampbase=0;
	X6502_Power();
	MDFN_PowerCheats();
}

static void RedoRL(void)
{
 EmulatedNES.DisplayRect.x = 0;
 EmulatedNES.DisplayRect.w = 256;

 if(PAL)
 {
  EmulatedNES.DisplayRect.y = MDFN_GetSettingUI("nes.slstartp");
  EmulatedNES.DisplayRect.h = MDFN_GetSettingUI("nes.slendp") - EmulatedNES.DisplayRect.y + 1;
 }
 else
 {
  EmulatedNES.DisplayRect.y = MDFN_GetSettingUI("nes.slstart");
  EmulatedNES.DisplayRect.h = MDFN_GetSettingUI("nes.slend") - EmulatedNES.DisplayRect.y + 1;
 }

 EmulatedNES.width = 256;

 if(MDFN_GetSettingUI("nes.clipsides"))
 {
  EmulatedNES.width -= 16;

  EmulatedNES.DisplayRect.x += 8;
  EmulatedNES.DisplayRect.w -= 16;
 }
}

static void ResetVidSys(void)
{
 int w;
  
 if(MDFNGameInfo->nes.vidsys == GIV_NTSC)
  w = 0; 
 else if(MDFNGameInfo->nes.vidsys == GIV_PAL)
  w = 1;  
 else
  w = MDFN_GetSettingB("nes.pal");
  
 PAL=w?1:0;
 MDFNPPU_SetVideoSystem(w);
 RedoRL();
 MDFNGameInfo->fps = PAL? 838977920 : 1008307711;
}

void MDFNNES_DrawOverlay(uint32 *XBuf)
{
	if(MDFNGameInfo->nes.type != GIT_NSF)
        {
          if(MDFNGameInfo->nes.type==GIT_VSUNI)
           MDFN_VSUniDraw(XBuf);
        }
	MDFN_DrawInput(XBuf);
}

static int StateAction(StateMem *sm, int load, int data_only)
{
 if(!X6502_StateAction(sm, load, data_only))
  return(0);

 if(!MDFNPPU_StateAction(sm, load, data_only))
  return(0);

 if(!MDFNSND_StateAction(sm, load, data_only))
  return(0);

 if(!load || load >= 0x0500)
 {
  if(!NESINPUT_StateAction(sm, load, data_only))
   return(0);
 }

 if(GameInterface->StateAction)
 {
  if(!GameInterface->StateAction(sm, load, data_only))
   return(0);
 }
 return(1);
}

static MDFNSetting NESSettings[] =
{
  { "nes.nofs", "Disabled four-score emulation.", MDFNST_BOOL, "0" },
  { "nes.no8lim", "No 8-sprites-per-scanline limit option.", MDFNST_BOOL, "0" },
  { "nes.fnscan", "Scan filename for (U),(J),(E),etc. strings to en/dis-able PAL emulation.", MDFNST_BOOL, "1" },
  { "nes.pal", "Enable PAL(50Hz) NES emulation.", MDFNST_BOOL, "0" },
  { "nes.gg", "Enable Game Genie emulation.", MDFNST_BOOL, "0" },
  { "nes.ggrom", "Path to Game Genie ROM image.", MDFNST_STRING, "" },
  { "nes.clipsides", "Clip left+right 8 pixel columns.", MDFNST_BOOL, "0" },
  { "nes.slstart", "First rendered scanline in NTSC mode.", MDFNST_UINT, "8" },
  { "nes.slend", "Last rendered scanlines in NTSC mode.", MDFNST_UINT, "231" },
  { "nes.slstartp", "First rendered scanline in PAL mode.", MDFNST_UINT, "0" },
  { "nes.slendp", "Last rendered scanlines in PAL mode.", MDFNST_UINT, "239" },
  { "nes.cpalette", "Filename of custom NES palette.", MDFNST_STRING, "" },
  { "nes.ntsccol", "Enable automatic generation and use of an NTSC NES' colors.", MDFNST_BOOL, "0" },
  { "nes.ntsctint", "NTSC NES color emulation mode \"Tint\" value.", MDFNST_UINT, "56", "0", "128" },
  { "nes.ntschue", "NTSC NES color emulation mode \"Hue\" value.", MDFNST_UINT, "72", "0", "128" },
  { NULL }
};

static bool StartNetplay(NetplaySystemInfoStruct *info)
{
 info->total_controllers = 4;
 info->controller_data_type = CONTROLLERDT_UINT8;
 return(1);
}

MDFNGI EmulatedNES =
{
 GISYS_NES,
 "nes",
 Load,
 NULL,
 CloseGame,
 MDFNNES_ToggleLayer,
 "Background\0Sprites\0",
 StateAction,
 DoRewind,
 Emulate,
 MDFNNES_SetPixelFormat,
 MDFNNES_SetInput,
 MDFNNES_SetSoundMultiplier,
 MDFNNES_SetSoundVolume,
 MDFNNES_Sound,
 MDFNNES_DoSimpleCommand,
 StartNetplay,
 NESSettings,
 0,
 NULL,
 256,
 240,
 256,
 256 * sizeof(uint32),
 { 0, 0, 256, 240 },
};
