#include <string.h>
#include "GBA.h"
#include "EEprom.h"
#include "../Util.h"

extern int cpuDmaCount;

int eepromMode = EEPROM_IDLE;
int eepromByte = 0;
int eepromBits = 0;
int eepromAddress = 0;

#ifdef __LIBRETRO__
// Workaround for broken-by-design GBA save semantics
extern u8 libretro_save_buf[0x20000 + 0x2000];
u8 *eepromData = libretro_save_buf + 0x20000;
#else
u8 eepromData[0x2000];
#endif

u8 eepromBuffer[16];
bool eepromInUse = false;
int eepromSize = 512;

variable_desc eepromSaveData[] = {
  { &eepromMode, sizeof(int) },
  { &eepromByte, sizeof(int) },
  { &eepromBits , sizeof(int) },
  { &eepromAddress , sizeof(int) },
  { &eepromInUse, sizeof(bool) },
  { &eepromData[0], 512 },
  { &eepromBuffer[0], 16 },
  { NULL, 0 }
};

void eepromInit()
{
#ifdef __LIBRETRO__
	memset(eepromData, 255, 0x2000);
#else
	memset(eepromData, 255, sizeof(eepromData));
#endif
}

void eepromReset()
{
  eepromMode = EEPROM_IDLE;
  eepromByte = 0;
  eepromBits = 0;
  eepromAddress = 0;
  eepromInUse = false;
  eepromSize = 512;
}

#ifdef __LIBRETRO__
void eepromSaveGame(uint8_t *& data)
{
   utilWriteDataMem(data, eepromSaveData);
   utilWriteIntMem(data, eepromSize);
   utilWriteMem(data, eepromData, 0x2000);
}

void eepromReadGame(const uint8_t *& data, int version)
{
   utilReadDataMem(data, eepromSaveData);
   if (version >= SAVE_GAME_VERSION_3) {
      eepromSize = utilReadIntMem(data);
      utilReadMem(eepromData, data, 0x2000);
   } else {
      // prior to 0.7.1, only 4K EEPROM was supported
      eepromSize = 512;
   }
}
#else
void eepromSaveGame(gzFile gzFile)
{
  utilWriteData(gzFile, eepromSaveData);
  utilWriteInt(gzFile, eepromSize);
  utilGzWrite(gzFile, eepromData, 0x2000);
}

void eepromReadGame(gzFile gzFile, int version)
{
  utilReadData(gzFile, eepromSaveData);
  if(version >= SAVE_GAME_VERSION_3) {
    eepromSize = utilReadInt(gzFile);
    utilGzRead(gzFile, eepromData, 0x2000);
  } else {
    // prior to 0.7.1, only 4K EEPROM was supported
    eepromSize = 512;
  }
}

void eepromReadGameSkip(gzFile gzFile, int version)
{
  // skip the eeprom data in a save game
  utilReadDataSkip(gzFile, eepromSaveData);
  if(version >= SAVE_GAME_VERSION_3) {
    utilGzSeek(gzFile, sizeof(int), SEEK_CUR);
    utilGzSeek(gzFile, 0x2000, SEEK_CUR);
  }
}
#endif

int eepromRead(u32 /* address */)
{
  switch(eepromMode) {
  case EEPROM_IDLE:
  case EEPROM_READADDRESS:
  case EEPROM_WRITEDATA:
    return 1;
  case EEPROM_READDATA:
    {
      eepromBits++;
      if(eepromBits == 4) {
        eepromMode = EEPROM_READDATA2;
        eepromBits = 0;
        eepromByte = 0;
      }
      return 0;
    }
  case EEPROM_READDATA2:
    {
      int data = 0;
      int address = eepromAddress << 3;
      int mask = 1 << (7 - (eepromBits & 7));
      data = (eepromData[address+eepromByte] & mask) ? 1 : 0;
      eepromBits++;
      if((eepromBits & 7) == 0)
        eepromByte++;
      if(eepromBits == 0x40)
        eepromMode = EEPROM_IDLE;
      return data;
    }
  default:
      return 0;
  }
  return 1;
}

void eepromWrite(u32 /* address */, u8 value)
{
  if(cpuDmaCount == 0)
    return;
  int bit = value & 1;
  switch(eepromMode) {
  case EEPROM_IDLE:
    eepromByte = 0;
    eepromBits = 1;
    eepromBuffer[eepromByte] = bit;
    eepromMode = EEPROM_READADDRESS;
    break;
  case EEPROM_READADDRESS:
    eepromBuffer[eepromByte] <<= 1;
    eepromBuffer[eepromByte] |= bit;
    eepromBits++;
    if((eepromBits & 7) == 0) {
      eepromByte++;
    }
    if(cpuDmaCount == 0x11 || cpuDmaCount == 0x51) {
      if(eepromBits == 0x11) {
        eepromInUse = true;
        eepromSize = 0x2000;
        eepromAddress = ((eepromBuffer[0] & 0x3F) << 8) |
          ((eepromBuffer[1] & 0xFF));
        if(!(eepromBuffer[0] & 0x40)) {
          eepromBuffer[0] = bit;
          eepromBits = 1;
          eepromByte = 0;
          eepromMode = EEPROM_WRITEDATA;
        } else {
          eepromMode = EEPROM_READDATA;
          eepromByte = 0;
          eepromBits = 0;
        }
      }
    } else {
      if(eepromBits == 9) {
        eepromInUse = true;
        eepromAddress = (eepromBuffer[0] & 0x3F);
        if(!(eepromBuffer[0] & 0x40)) {
          eepromBuffer[0] = bit;
          eepromBits = 1;
          eepromByte = 0;
          eepromMode = EEPROM_WRITEDATA;
        } else {
          eepromMode = EEPROM_READDATA;
          eepromByte = 0;
          eepromBits = 0;
        }
      }
    }
    break;
  case EEPROM_READDATA:
  case EEPROM_READDATA2:
    // should we reset here?
    eepromMode = EEPROM_IDLE;
    break;
  case EEPROM_WRITEDATA:
    eepromBuffer[eepromByte] <<= 1;
    eepromBuffer[eepromByte] |= bit;
    eepromBits++;
    if((eepromBits & 7) == 0) {
      eepromByte++;
    }
    if(eepromBits == 0x40) {
      eepromInUse = true;
      // write data;
      for(int i = 0; i < 8; i++) {
        eepromData[(eepromAddress << 3) + i] = eepromBuffer[i];
      }
      systemSaveUpdateCounter = SYSTEM_SAVE_UPDATED;
    } else if(eepromBits == 0x41) {
      eepromMode = EEPROM_IDLE;
      eepromByte = 0;
      eepromBits = 0;
    }
    break;
  }
}