/* Mednafen - Multi-system Emulator
 *
 *  Portions of this file Copyright (C) 2004 Ki
 *
 * 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 <errno.h>
#include <string.h>
#include "pce.h"
#include "cdrom.h"
#include "adpcm.h"
#include "../md5.h"
#include "../file.h"
#include "../cdromif.h"
#include "../mempatcher.h"

static uint8 *HuCROM;
static uint8 HuCROMMask;
static uint8 *ROMMap[0x100];

static bool IsPopulous;
bool PCE_IsCD;
bool PCE_SherlockHack = 0;

static uint8 PopRAM[0x8000];
static uint8 SaveRAM[2048];

static uint8 CDRAM[262144];

static uint8 ACRAM[0x200000]; // Yes, *2* MEGABYTES.  wow.
static uint8 ACRAMUsed;	      // Something to speed up save states.
static uint32 ACShift, ACShiftBits;
static uint8 AC1ae5;

typedef struct
{
	uint8 control;
	uint32 base;
	uint16 offset;
	uint16 increment;
} ACIO;

static ACIO AC[4];

static INLINE void increment_acaddr(ACIO* port)
{
        if (port->control & 1)          // CONFIRMED:  D0 enables base / offset increment
        {
                if (port->control & 0x10)       // CONFIRMED: D4 selects base / offset to be incremented
                {
                        port->base += port->increment;
                        port->base &= 0xffffff;
                }
                else
                {
                        port->offset += port->increment;
                }
        }
}

DECLFR(PCE_ACRead)
{
	ACIO *port = &AC[(A >> 4) & 0x3];
	uint8 ret;

	if((A & 0x1ae0) == 0x1ae0)
	{
	 switch(A & 0x1aef)
	 {
	  case 0x1ae0: return (uint8)ACShift;
	  case 0x1ae1: return (uint8)(ACShift >> 8);
	  case 0x1ae2: return (uint8)(ACShift >> 16);
	  case 0x1ae3: return (uint8)(ACShift >> 24);
	  case 0x1ae4: return (uint8)ACShiftBits;
	  case 0x1ae5: return(AC1ae5);
	  case 0x1aee: return(0x10);
	  case 0x1aef: return(0x51);
	 }
	 return(0xFF);
	}

	switch(A & 0xF)
	{
	 case 0x0:
	 case 0x1:
	 	if(port->control & 0x2)
		 ret = ACRAM[(port->base + port->offset) & 0x1fffff];
		else
		 ret = ACRAM[port->base & 0x1fffff];

		if(!PCE_InDebug)
		 increment_acaddr(port);

		return(ret);
	 case 0x2: return (uint8)port->base;
	 case 0x3: return (uint8)(port->base >> 8);
	 case 0x4: return (uint8)(port->base >> 16);
	 case 0x5: return (uint8)(port->offset);
	 case 0x6: return (uint8)(port->offset >> 8);
	 case 0x7: return (uint8)(port->increment);
	 case 0x8: return (uint8)(port->increment >> 8);
	 case 0x9: return(port->control);
	 case 0xa: return(0x00);
	}
	return(0xFF);
}

DECLFW(PCE_ACWrite)
{
        if ((A & 0x1ae0) == 0x1ae0)
        {
                switch (A & 0xf)
                {
                        case 0:
                                ACShift = (ACShift & ~0xff) | V;
                                return;
                        case 1:
                                ACShift = (ACShift & ~0xff00) | (V << 8);
                                return;
                        case 2:
                                ACShift = (ACShift & ~0xff0000) | (V << 16);
                                return;
                        case 3:
                                ACShift = (ACShift & ~0xff000000) | (V << 24);
                                return;
                        case 4:
                                if ((ACShiftBits = V & 0xf) != 0)
                                {
                                        if (ACShiftBits < 8)
                                                ACShift <<= ACShiftBits;
                                        else
                                                ACShift >>= 16 - ACShiftBits;
                                }
                                return;
                        case 5:
                                AC1ae5 = V;
                                return;
                }
        }
        else
        {
                ACIO            *port = &AC[(A >> 4) & 3];

                switch (A & 0xf)
                {
                        case 0x0:
                        case 0x1:
				ACRAMUsed = 1;
                                if (port->control & 2)
                                        ACRAM[(port->base + port->offset) & 0x1fffff] = V;
                                else
                                        ACRAM[port->base & 0x1fffff] = V;
                                increment_acaddr(port);
                                return;

                        case 0x2:
                                port->base = (port->base & ~0xff) | V;
                                return;
                        case 0x3:
                                port->base = (port->base & ~0xff00) | (V << 8);
                                return;
                        case 0x4:
                                port->base = (port->base & ~0xff0000) | (V << 16);
                                return;
                        case 0x5:
                                port->offset = (port->offset & ~0xff) | V;
                                return;
                        case 0x6:
                                port->offset = (port->offset & ~0xff00) | (V << 8);

                                if ((port->control & 0x60) == 0x40)
                                {
                                        if (port->control & 0x08)
                                                port->base += port->offset + 0xff0000;
                                        else
                                                port->base += port->offset;
                                        port->base &= 0xffffff;
                                }
                                return;
                        case 0x7:
                                port->increment = (port->increment & ~0xff) | V;
                                return;
                        case 0x8:
                                port->increment = (port->increment & ~0xff00) | (V << 8);
                                return;
                        case 0x9:
                                port->control = V & 0x7f;            // D7 is not used
                                return;
                        case 0xa:
                                // value written is not used
                                if ((port->control & 0x60) == 0x60)
                                {
                                        port->base += port->offset;
                                        port->base &= 0xffffff;
                                }
                                return;
                }
        }
}

static DECLFW(ACPhysWrite)
{
 PCE_ACWrite(0x1a00 | ((A >> 9) & 0x30), V);
}

static DECLFR(ACPhysRead)
{
 return(PCE_ACRead(0x1a00 | ((A >> 9) & 0x30)));
}

static DECLFW(CDRAMWrite)
{
 CDRAM[A - 0x68 * 8192] = V;
}

static DECLFR(CDRAMRead)
{
 return(CDRAM[A - 0x68 * 8192]);
}

static DECLFR(SaveRAMRead)
{
 if(!PCE_IsCD || CDROM_IsBRAMEnabled() && (A & 8191) < 2048)
  return(SaveRAM[A & 2047]);
 else
  return(0xFF);
}

static DECLFW(SaveRAMWrite)
{
 if(!PCE_IsCD || CDROM_IsBRAMEnabled() && (A & 8191) < 2048)
  SaveRAM[A & 2047] = V;
}

static DECLFR(HuCRead)
{
 return(ROMMap[A >> 13][A]);
}

static DECLFW(HuCRAMWrite)
{
 ROMMap[A >> 13][A] = V;
}

static uint8 HuCSF2Latch = 0;

static DECLFR(HuCSF2ReadLow)
{
 return(HuCROM[A]);
}

static DECLFR(HuCSF2Read)
{
 return(HuCROM[(A & 0x7FFFF) + 0x80000 + HuCSF2Latch * 0x80000 ]); // | (HuCSF2Latch << 19) ]);
}

static DECLFW(HuCSF2Write)
{
 if((A & 0x1FFC) == 0x1FF0)
 {
  HuCSF2Latch = (A & 0x3);
 }
}

int HuCLoad(uint8 *data, uint32 len, uint32 crc32)
{
 uint32 m_len = (len + 8191)&~8191;

 IsPopulous = 0;
 PCE_IsCD = 0;

 md5_context md5;
 md5.starts();
 md5.update(data, len);
 md5.finish(MDFNGameInfo->MD5);

 MDFN_printf(_("ROM:       %dKiB\n"), (len + 1023) / 1024);
 MDFN_printf(_("ROM CRC32: 0x%04x\n"), crc32);
 MDFN_printf(_("ROM MD5:   0x%s\n"), md5_context::asciistr(MDFNGameInfo->MD5, 0).c_str());

 HuCROM = (uint8 *)malloc(m_len);
 memset(HuCROM, 0xFF, m_len);
 memcpy(HuCROM, data, len);

 if(m_len == 0x60000)
 {
  for(int x = 0; x < 128; x++)
  {
   ROMMap[x] = HuCPUFastMap[x] = &HuCROM[(x & 0x1F) * 8192] - x * 8192;
   PCERead[x] = HuCRead;
  }

  for(int x = 64; x < 128; x++)
  {
   ROMMap[x] = HuCPUFastMap[x] = &HuCROM[((x & 0xF) + 32) * 8192] - x * 8192;
   PCERead[x] = HuCRead;
  }
 }
 else if(m_len == 0x80000)
 {
  for(int x = 0; x < 64; x++)
  {
   ROMMap[x] = HuCPUFastMap[x] = &HuCROM[(x & 0x3F) * 8192] - x * 8192;
   PCERead[x] = HuCRead;
  }
  for(int x = 64; x < 128; x++)
  {
   ROMMap[x] = HuCPUFastMap[x] = &HuCROM[((x & 0x1F) + 32) * 8192] - x * 8192;
   PCERead[x] = HuCRead;
  }
 }
 else
 {
  for(int x = 0; x < 128; x++)
  {
   uint8 bank = x % (m_len / 8192);
   ROMMap[x] = HuCPUFastMap[x] = &HuCROM[bank * 8192] - x * 8192;
   PCERead[x] = HuCRead;
  }
 }

 HuCROMMask = (m_len / 8192) - 1;

 if(!memcmp(HuCROM + 0x1F26, "POPULOUS", strlen("POPULOUS")))
 {
  gzFile fp;
  
  memset(PopRAM, 0xFF, 32768);
  if((fp = gzopen(MDFN_MakeFName(MDFNMKF_SAV, 0, "sav").c_str(), "rb")))
  {
   gzread(fp, PopRAM, 32768);
   gzclose(fp);
  }
  IsPopulous = 1;
  MDFN_printf("Populous\n");
  for(int x = 0x40; x < 0x44; x++)
  {
   ROMMap[x] = HuCPUFastMap[x] = &PopRAM[(x & 3) * 8192] - x * 8192;
   PCERead[x] = HuCRead;
   PCEWrite[x] = HuCRAMWrite;
  }
  MDFNMP_AddRAM(32768, 0x40 * 8192, PopRAM);
 }
 else
 {
  gzFile fp;

  memset(SaveRAM, 0x00, 2048);
  memcpy(SaveRAM, "HUBM\x00\xa0\x10\x80", 8);    // So users don't have to manually intialize the file cabinet
                                                // in the CD BIOS screen.
  if((fp = gzopen(MDFN_MakeFName(MDFNMKF_SAV, 0, "sav").c_str(), "rb")))
  {
   gzread(fp, SaveRAM, 2048);
   gzclose(fp);
  }
  PCEWrite[0xF7] = SaveRAMWrite;
  PCERead[0xF7] = SaveRAMRead;
  MDFNMP_AddRAM(2048, 0xF7 * 8192, SaveRAM);
 }

 // 0x1A558
 if(len >= 0x20000 && !memcmp(HuCROM + 0x1A558, "STREET FIGHTER#", strlen("STREET FIGHTER#")))
 {
  for(int x = 0x20; x < 0x40; x++)
   PCERead[x] = HuCSF2ReadLow;
  for(int x = 0x40; x < 0x80; x++)
  {
   HuCPUFastMap[x] = NULL; // Make sure our reads go through our read function, and not a table lookup
   PCERead[x] = HuCSF2Read;
  }
  PCEWrite[0] = HuCSF2Write;
  MDFN_printf("Street Fighter 2");
  HuCSF2Latch = 0;
 }

 return(1);
}
 
bool IsBRAMUsed(void)
{
 if(memcmp(SaveRAM, "HUBM\x00\xa0\x10\x80", 8)) // HUBM string is modified/missing
  return(1);

 for(int x = 8; x < 2048; x++)
  if(SaveRAM[x]) return(1);

 return(0);
}

int HuCLoadCD(const char *bios_path)
{
 MDFNFILE *fp = MDFN_fopen(bios_path, NULL, "rb", ".pce");
 if(!fp) 
 {
  MDFN_PrintError(_("Could not open CD BIOS file \"%s\": %s\n"), bios_path, strerror(errno));
  return(0);
 }
 HuCROM = (uint8 *)malloc(262144);
 memset(HuCROM, 0xFF, 262144);
 if(fp->size & 512)
  MDFN_fseek(fp, 512, SEEK_SET);
 MDFN_fread(HuCROM, 1, 262144, fp);
 MDFN_fclose(fp);

 ACRAMUsed = 0;
 PCE_IsCD = 1;
 CDROM_Init();
 ADPCM_Init();

 md5_context md5;
 md5.starts();
// md5_update(&md5, HuCROM, 262144);


 int32 track = CDIF_GetFirstTrack();
 int32 last_track = CDIF_GetLastTrack();
 bool DTFound = 0;
 for(; track <= last_track; track++)
 {
  CDIF_Track_Format format;

  if(CDIF_GetTrackFormat(track, format) && format == CDIF_FORMAT_MODE1)
  {
   DTFound = 1;
   break;
  }
 }
 
 PCE_SherlockHack = 0;
 if(DTFound) // Only add the MD5 hash if we were able to find a data track.
 {
  uint32 start_sector = CDIF_GetTrackStartPositionLBA(track);
  uint8 sector_buffer[2048];

  for(int x = 0; x < 128; x++)
  {
   memset(sector_buffer, 0, 2048);
   CDIF_ReadSector(sector_buffer, start_sector + x, 1);

   if(x == 12)
   {
    if(!memcmp((char *)sector_buffer + 0x209, "SHERLOCKMC", 10))
     PCE_SherlockHack = 1;
    //printf("Flumble: %s\n", (char *)sector_buffer + 0x209);
   }
   md5.update(sector_buffer, 2048);
  }
 }
 md5.finish(MDFNGameInfo->MD5);
 MDFN_printf(_("CD MD5(first 256KiB):   0x%s\n"), md5_context::asciistr(MDFNGameInfo->MD5, 0).c_str());

 for(int x = 0; x < 0x40; x++)
 {
  ROMMap[x] = HuCPUFastMap[x] = &HuCROM[(x & 0x1F) * 8192] - x * 8192;
  PCERead[x] = HuCRead;
 }

 for(int x = 0x68; x < 0x88; x++)
 {
  ROMMap[x] = HuCPUFastMap[x] = &CDRAM[(x - 0x68) * 8192] - x * 8192;
  PCERead[x] = CDRAMRead;
  PCEWrite[x] = CDRAMWrite;
 }
 MDFNMP_AddRAM(262144, 0x68 * 8192, CDRAM);

 for(int x = 0x40; x < 0x50; x++)
 {
  ROMMap[x] = HuCPUFastMap[x] = NULL;
  PCERead[x] = ACPhysRead;
  PCEWrite[x] = ACPhysWrite;
 }

 gzFile srp;

 memset(SaveRAM, 0x00, 2048);
 memcpy(SaveRAM, "HUBM\x00\xa0\x10\x80", 8);	// So users don't have to manually intialize the file cabinet
						// in the CD BIOS screen.

 if((srp = gzopen(MDFN_MakeFName(MDFNMKF_SAV, 0, "sav").c_str(), "rb")))
 {
  gzread(srp, SaveRAM, 2048);
  gzclose(srp);
 }
 PCEWrite[0xF7] = SaveRAMWrite;
 PCERead[0xF7] = SaveRAMRead;
 MDFNMP_AddRAM(2048, 0xF7 * 8192, SaveRAM);
 return(1);
}

int HuC_StateAction(StateMem *sm, int load, int data_only)
{
 SFORMAT StateRegs[] = 
 {
  SFARRAY(PopRAM, IsPopulous ? 32768 : 0),
  SFARRAY(SaveRAM, IsPopulous ? 0 : 2048),
  SFARRAY(CDRAM, PCE_IsCD ? 262144 : 0),
  SFVAR(HuCSF2Latch),
  SFVAR(ACRAMUsed),	// This needs to be saved/loaded before the ACStateRegs[] struct is saved/loaded.
  SFEND
 };
 int ret = MDFNSS_StateAction(sm, load, data_only, StateRegs, "HuC");
 if(load)
  HuCSF2Latch &= 0x3;


 SFORMAT ACStateRegs[] =
 {
  SFVAR(AC[0].control), SFVAR(AC[0].base), SFVAR(AC[0].offset), SFVAR(AC[0].increment),
  SFVAR(AC[1].control), SFVAR(AC[1].base), SFVAR(AC[1].offset), SFVAR(AC[1].increment),
  SFVAR(AC[2].control), SFVAR(AC[2].base), SFVAR(AC[2].offset), SFVAR(AC[2].increment),
  SFVAR(AC[3].control), SFVAR(AC[3].base), SFVAR(AC[3].offset), SFVAR(AC[3].increment),
  SFVAR(ACShiftBits),
  SFVAR(ACShift),
  SFVAR(AC1ae5),
  SFARRAY(ACRAM, ACRAMUsed ? 0x200000 : 0x0),
  SFEND
 };
 ret &= MDFNSS_StateAction(sm, load, data_only, ACStateRegs, "ACRD");
 if(PCE_IsCD)
 {
  ret &= ADPCM_StateAction(sm, load, data_only);
  ret &= CDROM_StateAction(sm, load, data_only);
 }
 return(ret);
}

void HuCClose(void)
{
 if(IsPopulous)
 {
  gzFile fp;
  if((fp = gzopen(MDFN_MakeFName(MDFNMKF_SAV, 0, "sav").c_str(), "wb")))
  {
   gzwrite(fp, PopRAM, 32768);
   gzclose(fp);
  }
 }
 else if(IsBRAMUsed())
 {
  gzFile fp;
  if((fp = gzopen(MDFN_MakeFName(MDFNMKF_SAV, 0, "sav").c_str(), "wb6")))
  {
   gzwrite(fp, SaveRAM, 2048);
   gzclose(fp);
  }
 }
}

void HuC_Power(void)
{
 if(CDRAM) memset(CDRAM, 0, 262144);
 if(ACRAM) memset(ACRAM, 0, 0x200000);
 ACRAMUsed = 0;
 ACShift = 0;
 ACShiftBits = 0;
 AC1ae5 = 0;
 memset(AC, 0, sizeof(AC));
}
