#include "../ui-base.hpp"
Cartridge cartridge;

//================
//public functions
//================

bool Cartridge::information(const char *filename, Cartridge::Information &info) {
  if(extension(filename) != "sfc") return false;  //do not parse compressed images

  file fp;
  if(fp.open(filename, file::mode_read) == false) return false;

  unsigned offset = 0;
  if((fp.size() & 0x7fff) == 512) offset = 512;

  uint16_t complement, checksum;

  fp.seek(0x7fdc + offset);
  complement = fp.readl(2);
  checksum = fp.readl(2);

  unsigned header = offset + (complement + checksum == 65535 ? 0x7fb0 : 0xffb0);

  fp.seek(header + 0x10);
  char name[22];
  fp.read((uint8_t*)name, 21);
  name[21] = 0;
  info.name = decodeShiftJIS(name);

  fp.seek(header + 0x29);
  uint8_t region = fp.read();
  info.region = (region <= 1 || region >= 13) ? "NTSC" : "PAL";

  info.romSize = fp.size() & ~0x7fff;

  fp.seek(header + 0x28);
  info.ramSize = fp.readl(1);
  if(info.ramSize) info.ramSize = 1024 << (info.ramSize & 7);

  fp.close();
  return true;
}

bool Cartridge::saveStatesSupported() {
  if(SNES::cartridge.mode() == SNES::Cartridge::Mode::Bsx) return false;

  if(SNES::cartridge.has_dsp3())   return false;
  if(SNES::cartridge.has_dsp4())   return false;
  if(SNES::cartridge.has_st0011()) return false;
  if(SNES::cartridge.has_st0018()) return false;
  if(SNES::cartridge.has_serial()) return false;

  return true;
}

bool Cartridge::loadNormal(const char *base) {
  unload();
  if(loadCartridge(baseName = base, cartridge.baseXml, SNES::memory::cartrom) == false) return false;
  SNES::cartridge.basename = nall::basename(baseName);

  SNES::cartridge.load(SNES::Cartridge::Mode::Normal,
    lstring() << cartridge.baseXml);

  loadMemory(baseName, ".srm", SNES::memory::cartram);
  loadMemory(baseName, ".rtc", SNES::memory::cartrtc);

  fileName = baseName;
  name = notdir(nall::basename(baseName));

  utility.modifySystemState(Utility::LoadCartridge);
  return true;
}

bool Cartridge::loadBsxSlotted(const char *base, const char *slot) {
  unload();
  if(loadCartridge(baseName = base, cartridge.baseXml, SNES::memory::cartrom) == false) return false;
  loadCartridge(slotAName = slot, cartridge.slotAXml, SNES::memory::bsxflash);
  SNES::cartridge.basename = nall::basename(baseName);

  SNES::cartridge.load(SNES::Cartridge::Mode::BsxSlotted,
    lstring() << cartridge.baseXml << cartridge.slotAXml);

  loadMemory(baseName, ".srm", SNES::memory::cartram);
  loadMemory(baseName, ".rtc", SNES::memory::cartrtc);

  fileName = baseName;
  name = notdir(nall::basename(baseName));
  if(*slot) name << " + " << notdir(nall::basename(slotAName));

  utility.modifySystemState(Utility::LoadCartridge);
  return true;
}

bool Cartridge::loadBsx(const char *base, const char *slot) {
  unload();
  if(loadCartridge(baseName = base, cartridge.baseXml, SNES::memory::cartrom) == false) return false;
  loadCartridge(slotAName = slot, cartridge.slotAXml, SNES::memory::bsxflash);
  SNES::cartridge.basename = nall::basename(baseName);

  SNES::cartridge.load(SNES::Cartridge::Mode::Bsx,
    lstring() << cartridge.baseXml << cartridge.slotAXml);

  loadMemory(baseName, ".srm", SNES::memory::bsxram );
  loadMemory(baseName, ".psr", SNES::memory::bsxpram);

  fileName = slotAName;
  name = *slot
  ? notdir(nall::basename(slotAName))
  : notdir(nall::basename(baseName));

  utility.modifySystemState(Utility::LoadCartridge);
  return true;
}

bool Cartridge::loadSufamiTurbo(const char *base, const char *slotA, const char *slotB) {
  unload();
  if(loadCartridge(baseName = base, cartridge.baseXml, SNES::memory::cartrom) == false) return false;
  loadCartridge(slotAName = slotA, cartridge.slotAXml, SNES::memory::stArom);
  loadCartridge(slotBName = slotB, cartridge.slotBXml, SNES::memory::stBrom);
  SNES::cartridge.basename = nall::basename(baseName);

  SNES::cartridge.load(SNES::Cartridge::Mode::SufamiTurbo,
    lstring() << cartridge.baseXml << cartridge.slotAXml << cartridge.slotBXml);

  loadMemory(slotAName, ".srm", SNES::memory::stAram);
  loadMemory(slotBName, ".srm", SNES::memory::stBram);

  fileName = slotAName;
  if(!*slotA && !*slotB) name = notdir(nall::basename(baseName));
  else if(!*slotB) name = notdir(nall::basename(slotAName));
  else if(!*slotA) name = notdir(nall::basename(slotBName));
  else name = notdir(nall::basename(slotAName)) << " + " << notdir(nall::basename(slotBName));

  utility.modifySystemState(Utility::LoadCartridge);
  return true;
}

bool Cartridge::loadSuperGameBoy(const char *base, const char *slot) {
  unload();
  if(loadCartridge(baseName = base, cartridge.baseXml, SNES::memory::cartrom) == false) return false;
  loadCartridge(slotAName = slot, cartridge.slotAXml, SNES::memory::gbrom);
  SNES::cartridge.basename = nall::basename(baseName);

  SNES::cartridge.load(SNES::Cartridge::Mode::SuperGameBoy,
    lstring() << cartridge.baseXml << cartridge.slotAXml);

  loadMemory(slotAName, ".sav", SNES::memory::gbram);
  loadMemory(slotBName, ".rtc", SNES::memory::gbrtc);

  fileName = slotAName;
  name = *slot
  ? notdir(nall::basename(slotAName))
  : notdir(nall::basename(baseName));

  utility.modifySystemState(Utility::LoadCartridge);
  return true;
}

void Cartridge::saveMemory() {
  if(SNES::cartridge.loaded() == false) return;

  switch(SNES::cartridge.mode()) {
    case SNES::Cartridge::Mode::Normal:
    case SNES::Cartridge::Mode::BsxSlotted: {
      saveMemory(baseName, ".srm", SNES::memory::cartram);
      saveMemory(baseName, ".rtc", SNES::memory::cartrtc);
    } break;

    case SNES::Cartridge::Mode::Bsx: {
      saveMemory(baseName, ".srm", SNES::memory::bsxram );
      saveMemory(baseName, ".psr", SNES::memory::bsxpram);
    } break;

    case SNES::Cartridge::Mode::SufamiTurbo: {
      saveMemory(slotAName, ".srm", SNES::memory::stAram);
      saveMemory(slotBName, ".srm", SNES::memory::stBram);
    } break;

    case SNES::Cartridge::Mode::SuperGameBoy: {
      saveMemory(slotAName, ".sav", SNES::memory::gbram);
      saveMemory(slotAName, ".rtc", SNES::memory::gbrtc);
    } break;
  }
}

void Cartridge::unload() {
  if(SNES::cartridge.loaded() == false) return;
  utility.modifySystemState(Utility::UnloadCartridge);
}

void Cartridge::loadCheats() {
  string name;
  name << filepath(nall::basename(baseName), config().path.cheat);
  name << ".cht";
  cheatEditorWindow->load(name);
}

void Cartridge::saveCheats() {
  string name;
  name << filepath(nall::basename(baseName), config().path.cheat);
  name << ".cht";
  cheatEditorWindow->save(name);
}

//=================
//private functions
//=================

bool Cartridge::loadCartridge(string &filename, string &xml, SNES::MappedRAM &memory) {
  if(file::exists(filename) == false) return false;

  uint8_t *data;
  unsigned size;
  audio.clear();
  if(reader.load(filename, data, size) == false) return false;

  patchApplied = false;
  string name(filepath(nall::basename(filename), config().path.patch), ".ups");

  file fp;
  if(config().file.applyPatches && fp.open(name, file::mode_read)) {
    unsigned patchsize = fp.size();
    uint8_t *patchdata = new uint8_t[patchsize];
    fp.read(patchdata, patchsize);
    fp.close();

    uint8_t *outdata = 0;
    unsigned outsize = 0;
    ups patcher;
    ups::result result = patcher.apply(patchdata, patchsize, data, size, outdata, outsize);
    delete[] patchdata;

    bool apply = false;
    if(result == ups::ok) apply = true;
    if(config().file.bypassPatchCrc32) {
      if(result == ups::input_crc32_invalid ) apply = true;
      if(result == ups::output_crc32_invalid) apply = true;
    }

    if(apply == true) {
      delete[] data;
      data = outdata;
      size = outsize;
      patchApplied = true;
    } else {
      delete[] outdata;
    }
  }

  name = string(nall::basename(filename), ".xml");
  if(file::exists(name)) {
    //prefer manually created XML cartridge mapping
    xml.readfile(name);
  } else {
    //generate XML mapping from data via heuristics
    xml = snes_information(data, size).xml_memory_map;
  }

  memory.copy(data, size);
  delete[] data;
  return true;
}

bool Cartridge::loadMemory(const char *filename, const char *extension, SNES::MappedRAM &memory) {
  if(memory.size() == 0 || memory.size() == -1U) return false;

  string name;
  name << filepath(nall::basename(filename), config().path.save);
  name << extension;

  file fp;
  if(fp.open(name, file::mode_read) == false) return false;

  unsigned size = fp.size();
  uint8_t *data = new uint8_t[size];
  fp.read(data, size);
  fp.close();

  memory.copy(data, size);
  delete[] data;
  return true;
}

bool Cartridge::saveMemory(const char *filename, const char *extension, SNES::MappedRAM &memory) {
  if(memory.size() == 0 || memory.size() == -1U) return false;

  string name;
  name << filepath(nall::basename(filename), config().path.save);
  name << extension;

  file fp;
  if(fp.open(name, file::mode_write) == false) return false;

  fp.write(memory.data(), memory.size());
  fp.close();
  return true;
}

string Cartridge::decodeShiftJIS(const char *text) {
  unsigned length = strlen(text), offset = 0;
  string output;

  for(unsigned i = 0; i < length;) {
    unsigned code = 0;
    uint8_t n = text[i++];

    if(n == 0x00) {
      //string terminator
      break;
    } else if(n >= 0x20 && n <= 0x7f) {
      //ASCII
      code = n;
    } else if(n >= 0xa0 && n <= 0xdf) {
      //ShiftJIS half-width katakana
      unsigned dakuten = 0, handakuten = 0;

      switch(n) {
        case 0xa1: code = 0xe38082; break;  //(period)
        case 0xa2: code = 0xe3808c; break;  //(open quote)
        case 0xa3: code = 0xe3808d; break;  //(close quote)
        case 0xa4: code = 0xe38081; break;  //(comma)
        case 0xa5: code = 0xe383bb; break;  //(separator)
        case 0xa6: code = 0xe383b2; break;  //wo
        case 0xa7: code = 0xe382a1; break;  //la
        case 0xa8: code = 0xe382a3; break;  //li
        case 0xa9: code = 0xe382a5; break;  //lu
        case 0xaa: code = 0xe382a7; break;  //le
        case 0xab: code = 0xe382a9; break;  //lo
        case 0xac: code = 0xe383a3; break;  //lya
        case 0xad: code = 0xe383a5; break;  //lyu
        case 0xae: code = 0xe383a7; break;  //lyo
        case 0xaf: code = 0xe38383; break;  //ltsu
        case 0xb0: code = 0xe383bc; break;  //-
        case 0xb1: code = 0xe382a2; break;  //a
        case 0xb2: code = 0xe382a4; break;  //i
        case 0xb3: code = 0xe382a6; break;  //u 
        case 0xb4: code = 0xe382a8; break;  //e
        case 0xb5: code = 0xe382aa; break;  //o
        case 0xb6: code = 0xe382ab; dakuten = 0xe382ac; break;  //ka,  ga
        case 0xb7: code = 0xe382ad; dakuten = 0xe382ae; break;  //ki,  gi
        case 0xb8: code = 0xe382af; dakuten = 0xe382b0; break;  //ku,  gu
        case 0xb9: code = 0xe382b1; dakuten = 0xe382b2; break;  //ke,  ge
        case 0xba: code = 0xe382b3; dakuten = 0xe382b4; break;  //ko,  go
        case 0xbb: code = 0xe382b5; dakuten = 0xe382b6; break;  //sa,  za
        case 0xbc: code = 0xe382b7; dakuten = 0xe382b8; break;  //shi, zi
        case 0xbd: code = 0xe382b9; dakuten = 0xe382ba; break;  //su,  zu
        case 0xbe: code = 0xe382bb; dakuten = 0xe382bc; break;  //se,  ze
        case 0xbf: code = 0xe382bd; dakuten = 0xe382be; break;  //so,  zo
        case 0xc0: code = 0xe382bf; dakuten = 0xe38380; break;  //ta,  da
        case 0xc1: code = 0xe38381; dakuten = 0xe38382; break;  //chi, di
        case 0xc2: code = 0xe38384; dakuten = 0xe38385; break;  //tsu, du
        case 0xc3: code = 0xe38386; dakuten = 0xe38387; break;  //te,  de
        case 0xc4: code = 0xe38388; dakuten = 0xe38389; break;  //to,  do
        case 0xc5: code = 0xe3838a; break;  //na
        case 0xc6: code = 0xe3838b; break;  //ni
        case 0xc7: code = 0xe3838c; break;  //nu
        case 0xc8: code = 0xe3838d; break;  //ne
        case 0xc9: code = 0xe3838e; break;  //no
        case 0xca: code = 0xe3838f; dakuten = 0xe38390; handakuten = 0xe38391; break;  //ha, ba, pa
        case 0xcb: code = 0xe38392; dakuten = 0xe38393; handakuten = 0xe38394; break;  //hi, bi, pi
        case 0xcc: code = 0xe38395; dakuten = 0xe38396; handakuten = 0xe38397; break;  //fu, bu, pu
        case 0xcd: code = 0xe38398; dakuten = 0xe38399; handakuten = 0xe3839a; break;  //he, be, pe
        case 0xce: code = 0xe3839b; dakuten = 0xe3839c; handakuten = 0xe3839d; break;  //ho, bo, po
        case 0xcf: code = 0xe3839e; break;  //ma
        case 0xd0: code = 0xe3839f; break;  //mi
        case 0xd1: code = 0xe383a0; break;  //mu
        case 0xd2: code = 0xe383a1; break;  //me
        case 0xd3: code = 0xe383a2; break;  //mo
        case 0xd4: code = 0xe383a4; break;  //ya
        case 0xd5: code = 0xe383a6; break;  //yu
        case 0xd6: code = 0xe383a8; break;  //yo
        case 0xd7: code = 0xe383a9; break;  //ra
        case 0xd8: code = 0xe383aa; break;  //ri
        case 0xd9: code = 0xe383ab; break;  //ru
        case 0xda: code = 0xe383ac; break;  //re
        case 0xdb: code = 0xe383ad; break;  //ro
        case 0xdc: code = 0xe383af; break;  //wa
        case 0xdd: code = 0xe383b3; break;  //n
      }

      if(dakuten && ((uint8_t)text[i] == 0xde)) {
        code = dakuten;
        i++;
      } else if(handakuten && ((uint8_t)text[i] == 0xdf)) {
        code = handakuten;
        i++;
      }
    }

    if(code) {
      if((uint8_t)(code >> 16)) output[offset++] = (char)(code >> 16);
      if((uint8_t)(code >>  8)) output[offset++] = (char)(code >>  8);
      if((uint8_t)(code >>  0)) output[offset++] = (char)(code >>  0);
    }
  }

  output[offset] = 0;
  return output;
}
