/* Mednafen - Multi-system Emulator
 *
 * 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 <zlib.h>
#include "pce.h"
#include "vdc.h"
#include "psg.h"
#include "input.h"
#include "huc.h"
#include "cdrom.h"
#include "hes.h"
#include "debug.h"
#include "../netplay.h"
#include "../mempatcher.h"

static RegType SGXRegsVDC[] =
{
        { "MAWR-B", "Memory Write Address, VDC-B", 2 },
        { "MARR-B", "Memory Read Address, VDC-B", 2 },
        { "CR-B", "Control, VDC-B", 2 },
        { "RCR-B", "Raster Counter, VDC-B", 2 },
        { "BXR-B", "X Scroll, VDC-B", 2 },
        { "BYR-B", "Y Scroll, VDC-B", 2 },
        { "MWR-B", "Memory Width, VDC-B", 2 },

        { "HSR-B", "HSR, VDC-B", 2 },
        { "HDR-B", "HDR, VDC-B", 2 },
        { "VSR-B", "VSR, VDC-B", 2 },
        { "VDR-B", "VDR, VDC-B", 2 },

        { "VCR-B", "VCR, VDC-B", 2 },
        { "DCR-B", "DMA Control, VDC-B", 2 },
        { "SOUR-B", "VRAM DMA Source Address, VDC-B", 2 },
        { "DESR-B", "VRAM DMA Dest Address, VDC-B", 2 },
        { "LENR-B", "VRAM DMA Length, VDC-B", 2 },
        { "SATB-B", "SATB Update Address, VDC-B", 2 },
        { "", "", 0 },
};

bool PCE_InDebug = 0;

extern MDFNGI EmulatedPCE;
static bool IsSGX;
static bool IsHES;
int pce_overclocked;

static uint8 BaseRAM[32768]; // 8KB for PCE, 32KB for Super Grafx

uint8 PCEIODataBuffer;
readfunc PCERead[0x100], NonCheatPCERead[0x100];
writefunc PCEWrite[0x100];

static DECLFR(PCEBusRead)
{
 //printf("BUS Read: %02x %04x\n", A >> 13, A);
 return(0xFF);
}

static DECLFW(PCENullWrite)
{
 //printf("Null Write: %02x, %08x %02x\n", A >> 13, A, V);
}

static DECLFR(BaseRAMReadSGX)
{
 return(BaseRAM[A & 0x7FFF]);
}

static DECLFW(BaseRAMWriteSGX)
{
 BaseRAM[A & 0x7FFF] = V;
}

static DECLFR(BaseRAMRead)
{
 return(BaseRAM[A & 0x1FFF]);
}

static DECLFW(BaseRAMWrite)
{
 BaseRAM[A & 0x1FFF] = V;
}

static DECLFR(IORead)
{
 A &= 0x1FFF;

 switch(A & 0x1c00)
 {
  case 0x0000: if(!PCE_InDebug) HuC6280_StealCycles(1); 
	       return(VDC_Read(A));
  case 0x0400: if(!PCE_InDebug) HuC6280_StealCycles(1); 
	       return(VCE_Read(A));
  case 0x0800: if(HuCPU.in_block_move)
		return(0);
	       return(PCEIODataBuffer); 
  case 0x0c00: if(HuCPU.in_block_move)
                return(0);
	       {
		uint8 ret = HuC6280_TimerRead(A);
                if(!PCE_InDebug) PCEIODataBuffer = ret;
                return(ret);
               }
  case 0x1000: if(HuCPU.in_block_move)
                return(0);
	       {
	        uint8 ret = INPUT_Read(A);
                if(!PCE_InDebug) PCEIODataBuffer = ret;
                return(ret);
               }
  case 0x1400: if(HuCPU.in_block_move)
                return(0);
	       {
	        uint8 ret = HuC6280_IRQStatusRead(A);
		if(!PCE_InDebug) PCEIODataBuffer = ret;
	        return(ret);
	       }
  case 0x1800: if(!PCE_IsCD) break;
	       if((A & 0x1E00) == 0x1A00)
		return(PCE_ACRead(A));
	       else
		return(CDROM_Read(A));
  case 0x1C00: if(IsHES) return(ReadIBP(A)); 
	       break; // Expansion
  //default: printf("Eeep\n");break;
 }
 return(0xFF);
}

static DECLFW(IOWrite)
{
 A &= 0x1FFF;
  
 switch(A & 0x1c00)
 {
  case 0x0000: if(!PCE_InDebug) HuC6280_StealCycles(1); 
	       VDC_Write(A, V); break;
  case 0x0400: if(!PCE_InDebug) HuC6280_StealCycles(1); 
	       VCE_Write(A, V); break;
  case 0x0800: PCEIODataBuffer = V; PSG_Write(A, V); break;
  case 0x0c00: PCEIODataBuffer = V; HuC6280_TimerWrite(A, V); break;
  case 0x1000: PCEIODataBuffer = V; INPUT_Write(A, V); break;
  case 0x1400: PCEIODataBuffer = V; HuC6280_IRQStatusWrite(A, V); break;
  case 0x1800: if(!PCE_IsCD) break;
	       if((A & 0x1E00) == 0x1A00)
		PCE_ACWrite(A, V);
	       else
	        CDROM_Write(A, V); 
	       break;
  //case 0x1C00: break; // Expansion
  //default: printf("Eep: %04x\n", A); break;
 }

}

static int LoadCommon(void);
static int Load(const char *name, MDFNFILE *fp)
{
 uint32 headerlen = 0;
 uint32 r_size;

 memset(HuCPUFastMap, 0, sizeof(HuCPUFastMap));

 IsHES = 0;

 if(!memcmp(fp->data, "HESM", 4))
  IsHES = 1;

 if(strcasecmp(fp->ext, "pce") && !IsHES)
  return(-1);

 if(!IsHES)
 {
  if(fp->size & 0x200) // 512 byte header!
   headerlen = 512;
 }

 MDFNMP_Init(1024, (1 << 21) / 1024);

 r_size = fp->size - headerlen;
 if(r_size > 4096 * 1024) r_size = 4096 * 1024;

 for(int x = 0; x < 0x100; x++)
 {
  PCERead[x] = PCEBusRead;
  PCEWrite[x] = PCENullWrite;
 }

 uint32 crc = crc32(0, fp->data + headerlen, fp->size - headerlen);


 if(IsHES)
 {
  if(!PCE_HESLoad(fp->data, fp->size))
   return(0);
 }
 else
  HuCLoad(fp->data + headerlen, fp->size - headerlen, crc);

 if(fp->size >= 8192 && !memcmp(fp->data + headerlen, "DARIUS Version 1.11b", strlen("DARIUS VERSION 1.11b")))
 {
  MDFN_printf("SuperGfx:  Darius Plus\n");
  IsSGX = 1;
 }

 if(crc == 0x4c2126b0)
 {
  MDFN_printf("SuperGfx:  Aldynes\n");
  IsSGX = 1;
 }

 if(crc == 0x8c4588e2)
 {
  MDFN_printf("SuperGfx:  1941 - Counter Attack\n");
  IsSGX = 1;
 }
 if(crc == 0x1f041166)
 {
  MDFN_printf("SuperGfx:  Madouou Granzort\n");
  IsSGX = 1;
 }
 if(crc == 0xb486a8ed)
 {
  MDFN_printf("SuperGfx:  Daimakaimura\n");
  IsSGX = 1;
 }
 if(crc == 0x3b13af61)
 {
  MDFN_printf("SuperGfx:  Battle Ace\n");
  IsSGX = 1;
 }

 return(LoadCommon());
}

static int LoadCommon(void)
{ 
 IsSGX |= MDFN_GetSettingB("pce.forcesgx") ? 1 : 0;
 if(IsHES)
  IsSGX = 1;

 pce_overclocked = MDFN_GetSettingUI("pce.ocmultiplier");
 if(pce_overclocked > 1)
  MDFN_printf("CPU overclock: %dx\n", pce_overclocked);
 if(MDFN_GetSettingUI("pce.cdspeed") > 1)
  MDFN_printf("CD-ROM speed:  %dx\n", MDFN_GetSettingUI("pce.cdspeed"));
 VDC_Init(IsSGX);

 if(IsSGX)
 {
  MDFN_printf("SuperGrafx Emulation Enabled.\n");
  PCERead[0xF8] = PCERead[0xF9] = PCERead[0xFA] = PCERead[0xFB] = BaseRAMReadSGX;
  PCEWrite[0xF8] = PCEWrite[0xF9] = PCEWrite[0xFA] = PCEWrite[0xFB] = BaseRAMWriteSGX;

  for(int x = 0xf8; x < 0xfb; x++)
   HuCPUFastMap[x] = BaseRAM - 0xf8 * 8192;
 }
 else
 {
  PCERead[0xF8] = PCERead[0xF9] = PCERead[0xFA] = PCERead[0xFB] = BaseRAMRead;
  PCEWrite[0xF8] = PCEWrite[0xF9] = PCEWrite[0xFA] = PCEWrite[0xFB] = BaseRAMWrite;

  for(int x = 0xf8; x < 0xfb; x++)
   HuCPUFastMap[x] = BaseRAM - x * 8192;
 }

 MDFNMP_AddRAM(IsSGX ? 32768 : 8192, 0xf8 * 8192, BaseRAM);

 PCERead[0xFF] = IORead;
 PCEWrite[0xFF] = IOWrite;

 HuC6280_Init();

 MDFNGameInfo->pitch = 1024 * sizeof(uint32);

 if(MDFN_GetSettingB("pce.forcemono"))
 {
  MDFNGameInfo->soundchan = 1;
  PSG_Init(1);
 }
 else
 {
  MDFNGameInfo->soundchan = 2;
  PSG_Init(0);
 }

 PCE_Power();

 MDFNGameInfo->LayerNames = IsSGX ? "BG0\0SPR0\0BG1\0SPR1\0" : "Background\0Sprites\0";
 MDFNGameInfo->fps = (uint32)((double)7159090.90909090 / 455 / 263 * 65536 * 256);


 memcpy(NonCheatPCERead, PCERead, sizeof(PCERead));

 #ifdef WANT_DEBUGGER
 MDFNGameInfo->Debugger->Regs[2] = IsSGX ? SGXRegsVDC : NULL;
 #endif

 MDFN_LoadGameCheats(0);
 MDFNMP_InstallReadPatches();
 return(1);
}

static int LoadCD(void)
{
 IsHES = 0;
 IsSGX = 0;

 for(int x = 0; x < 0x100; x++)
 {
  PCERead[x] = PCEBusRead;
  PCEWrite[x] = PCENullWrite;
 }

 MDFNMP_Init(1024, (1 << 21) / 1024);

 if(!HuCLoadCD(MDFN_GetSettingS("pce.cdbios").c_str()))
  return(0);

 return(LoadCommon());
}


static void CloseGame(void)
{
 if(IsHES)
  HES_Close();
 else
 {
  MDFN_FlushGameCheats(0);
  HuCClose();
 }
}

static int needrew = 0;

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

static void Emulate(uint32 *pXBuf, MDFN_Rect *LineWidths, uint32 *pHLOverlayBuf, MDFN_Rect *HLOverlayLineWidths, int16 **SoundBuf, int32 *SoundBufSize, int skip)
{
 int didrew;

 MDFNGameInfo->fb = pXBuf;
 INPUT_Frame();

 didrew = MDFN_StateEvil(needrew);

 MDFNMP_ApplyPeriodicCheats();

 VDC_RunFrame(pXBuf, LineWidths, IsHES ? 1 : skip);

 *SoundBuf = PSG_Flush(SoundBufSize, didrew);

 needrew = 0;

 if(IsHES && !skip)
  HES_Draw(pXBuf, *SoundBuf, *SoundBufSize);
}

static int StateAction(StateMem *sm, int load, int data_only)
{
 SFORMAT StateRegs[] =
 {
  SFARRAY(BaseRAM, IsSGX? 32768 : 8192),
  SFVAR(PCEIODataBuffer),
  SFEND
 };

 int ret = MDFNSS_StateAction(sm, load, data_only, StateRegs, "MAIN");

 ret &= HuC6280_StateAction(sm, load, data_only);
 ret &= VDC_StateAction(sm, load, data_only);
 ret &= PSG_StateAction(sm, load, data_only);
 ret &= INPUT_StateAction(sm, load, data_only);
 ret &= HuC_StateAction(sm, load, data_only);

 if(load)
 {

 }

 return(ret);
}

static void PCE_Reset(void)
{
 PCEIODataBuffer = 0xFF;
 HuC6280_Reset();
 VDC_Reset();
 PSG_Reset();

 if(IsHES)
  HES_Reset();
 if(PCE_IsCD)
  CDROM_Reset();
}

void PCE_Power(void)
{
 memset(BaseRAM, 0x00, sizeof(BaseRAM));
 PCEIODataBuffer = 0xFF;

 HuC6280_Power();
 VDC_Power();
 PSG_Power();
 HuC_Power();
 if(IsHES)
  HES_Reset();
 if(PCE_IsCD)
  CDROM_Reset();
}

static void DoSimpleCommand(int cmd)
{
 switch(cmd)
 {
  case MDFNNPCMD_RESET: PCE_Reset(); break;
  case MDFNNPCMD_POWER: PCE_Power(); break;
 }
}

static MDFNSetting PCESettings[] = 
{
  { "pce.input.port1", gettext_noop("Select input device for input port 1."), MDFNST_STRING, "gamepad", NULL, NULL },
  { "pce.input.port2", gettext_noop("Select input device for input port 2."), MDFNST_STRING, "gamepad", NULL, NULL },
  { "pce.input.port3", gettext_noop("Select input device for input port 3."), MDFNST_STRING, "gamepad", NULL, NULL },
  { "pce.input.port4", gettext_noop("Select input device for input port 4."), MDFNST_STRING, "gamepad", NULL, NULL },
  { "pce.input.port5", gettext_noop("Select input device for input port 5."), MDFNST_STRING, "gamepad", NULL, NULL },
  { "pce.mouse_sensitivity", gettext_noop("Set mouse sensitivity."), MDFNST_FLOAT, "0.50", NULL, NULL },
  { "pce.forcemono", gettext_noop("Force monophonic sound output."), MDFNST_BOOL, "0" },
  { "pce.forcesgx", gettext_noop("Force SuperGrafx emulation."), MDFNST_BOOL, "0" },
  { "pce.ocmultiplier", gettext_noop("CPU overclock multiplier."), MDFNST_UINT, "1", "1", "50"},
  { "pce.cdspeed", gettext_noop("CD-ROM data transfer speed multiplier."), MDFNST_UINT, "1", "1", "100" },
  { "pce.nospritelimit", gettext_noop("No 16-sprites-per-scanline limit option."), MDFNST_BOOL, "0" },
  { "pce.cdbios", gettext_noop("Path to the CD BIOS"), MDFNST_STRING, "pce.cdbios PATH NOT SET" },
  { "pce.adpcmlp", gettext_noop("Enable lowpass filter dependent on playback-frequency."), MDFNST_BOOL, "0" },
  { NULL }
};

static DECLFR(CheatReadFunc)
{
  std::vector<SUBCHEAT>::iterator chit;
  uint8 retval = NonCheatPCERead[(A / 8192) & 0xFF](A);

  for(chit = SubCheats[A & 0x7].begin(); chit != SubCheats[A & 0x7].end(); chit++)
  {
   if(A == chit->addr)
   {
    if(chit->compare == -1 || chit->compare == retval)
    {
     retval = chit->value;
     break;
    }
   }
  }
 return(retval);
}

static uint8 MemRead(uint32 addr)
{
 return(NonCheatPCERead[(addr / 8192) & 0xFF](addr));
}

static void InstallReadPatch(uint32 address)
{
 HuCPUFastMap[address / 8192] = NULL;
 PCERead[address / 8192] = CheatReadFunc;
}

static void RemoveReadPatches(void)
{
 for(int x = 0; x < 0x100; x++)
  PCERead[x] = NonCheatPCERead[x];
}

#ifdef WANT_DEBUGGER
static RegType PCERegs[] =
{
        { "PC", "Program Counter", 2 },
        { "A", "Accumulator", 1 },
        { "X", "X Index Register", 1 },
        { "Y", "Y Index Register", 1 },
        { "SP", "Stack Pointer", 1 },
        { "P", "Status", 1 },
	{ "MPR0", "MPR0", 1},
	{ "MPR1", "MPR1", 1},
	{ "MPR2", "MPR2", 1},
	{ "MPR3", "MPR3", 1},
	{ "MPR4", "MPR4", 1},
	{ "MPR5", "MPR5", 1},
	{ "MPR6", "MPR6", 1},
	{ "MPR7", "MPR7", 1},
	{ "SPD", "CPU Speed", 1 },
	{ "IRQM", "IRQ Mask", 1},
	{ "TIMS", "Timer Status", 1},
	{ "TIMV", "Timer Value", 1},
	{ "TIML", "Timer Load", 1},
	{ "TIMD", "Timer Div Counter", 2},
	{ "", "", 0 },
};

static RegType PCERegsVDC[] =
{
        { "MAWR", "Memory Write Address", 2 },
        { "MARR", "Memory Read Address", 2 },
        { "CR", "Control", 2 },
        { "RCR", "Raster Counter", 2 },
        { "BXR", "X Scroll", 2 },
        { "BYR", "Y Scroll", 2 },
        { "MWR", "Memory Width", 2 },

        { "HSR", "HSR", 2 },
        { "HDR", "HDR", 2 },
        { "VSR", "VSR", 2 },
        { "VDR", "VDR", 2 },

        { "VCR", "VCR", 2 },
        { "DCR", "DMA Control", 2 },
        { "SOUR", "VRAM DMA Source Address", 2 },
        { "DESR", "VRAM DMA Dest Address", 2 },
        { "LENR", "VRAM DMA Length", 2 },
        { "SATB", "SATB Update Address", 2 },
	
	{ "Frm Cntr", "Frame Counter", 2 },
        { "", "", 0 },
};

static DebuggerInfoStruct DBGInfo =
{
 { PCERegs, PCERegsVDC, NULL, NULL },
 7,
 16,
 21,
 0x2000,
 PCEDBG_MemPeek,
 PCEDBG_MemPoke,
 VDC_GfxMemPeek,
 VDC_GfxMemPoke,
 PCEDBG_Disassemble,
 NULL, //NESDBG_IRQ,
 NULL, //NESDBG_GetVector,
 PCEDBG_GetRegister,
 PCEDBG_SetRegister,
 PCEDBG_FlushBreakPoints,
 PCEDBG_AddBreakPoint,
 PCEDBG_SetCPUCallback,
 PCEDBG_SetBPCallback,
 PCEDBG_GetBranchTrace,
 VDC_SetGraphicsDecode,
 VDC_GetGraphicsDecodeBuffer
};
#endif

MDFNGI EmulatedPCE =
{
 "pce",
 #ifdef WANT_DEBUGGER
 &DBGInfo,
 #endif
 &PCEInputInfo,
 Load,
 LoadCD,
 CloseGame,
 VDC_ToggleLayer,
 NULL,
 InstallReadPatch,
 RemoveReadPatches,
 MemRead,
 StateAction,
 DoRewind,
 Emulate,
 VDC_SetPixelFormat,
 PCEINPUT_SetInput,
 NULL,
 NULL,
 NULL,
 PSG_SetSoundMultiplier,
 PSG_SetSoundVolume,
 PSG_Sound,
 DoSimpleCommand,
 PCESettings,
 0,
 NULL,
 320,
 232,
 256, // Save state preview width
 1024 * sizeof(uint32),
 { 0, 4, 320, 232 },
 FALSE
};

