#include <sfc/sfc.hpp>

namespace SuperFamicom {

Interface *interface = nullptr;

double Interface::videoFrequency() {
  switch(system.region()) { default:
  case System::Region::NTSC: return system.cpu_frequency() / (262.0 * 1364.0 - 4.0);
  case System::Region::PAL:  return system.cpu_frequency() / (312.0 * 1364.0);
  }
}

double Interface::audioFrequency() {
  return system.apu_frequency() / 768.0;
}

bool Interface::loaded() {
  return cartridge.loaded();
}

string Interface::sha256() {
  return cartridge.sha256();
}

unsigned Interface::group(unsigned id) {
  switch(id) {
  case ID::IPLROM:
    return 0;
  case ID::ROM:
  case ID::RAM:
  case ID::SA1ROM:
  case ID::SA1IRAM:
  case ID::SA1BWRAM:
  case ID::SuperFXROM:
  case ID::SuperFXRAM:
  case ID::ArmDSP:
  case ID::HitachiDSP:
  case ID::HitachiDSPROM:
  case ID::Nec7725DSP:
  case ID::Nec96050DSP:
  case ID::NecDSPRAM:
  case ID::EpsonRTC:
  case ID::SharpRTC:
  case ID::SPC7110PROM:
  case ID::SPC7110DROM:
  case ID::SPC7110RAM:
  case ID::SDD1ROM:
  case ID::SDD1RAM:
  case ID::OBC1RAM:
  case ID::SuperGameBoyBootROM:
  case ID::BsxROM:
  case ID::BsxRAM:
  case ID::BsxPSRAM:
    return 1;
  case ID::SuperGameBoy:
  case ID::SuperGameBoyROM:
  case ID::SuperGameBoyRAM:
    return 2;
  case ID::Satellaview:
  case ID::BsxFlashROM:
    return 3;
  case ID::SufamiTurboSlotA:
  case ID::SufamiTurboSlotAROM:
  case ID::SufamiTurboSlotARAM:
    return 4;
  case ID::SufamiTurboSlotB:
  case ID::SufamiTurboSlotBROM:
  case ID::SufamiTurboSlotBRAM:
    return 5;
  }

  print(id, "\n");
  return 1;
  throw;
}

void Interface::load(unsigned id, const string &manifest) {
  if(id == ID::SuperFamicom) cartridge.load(manifest);
  if(id == ID::SuperGameBoy) cartridge.load_super_game_boy(manifest);
  if(id == ID::Satellaview) cartridge.load_satellaview(manifest);
  if(id == ID::SufamiTurboSlotA) cartridge.load_sufami_turbo_a(manifest);
  if(id == ID::SufamiTurboSlotB) cartridge.load_sufami_turbo_b(manifest);
}

void Interface::save() {
  for(auto &memory : cartridge.memory) {
    saveRequest(memory.id, memory.name);
  }
}

void Interface::load(unsigned id, const stream &stream, const string &manifest) {
  if(id == ID::IPLROM) {
    stream.read(smp.iplrom, min(64u, stream.size()));
  }

  if(id == ID::ROM) cartridge.rom.read(stream);
  if(id == ID::RAM) cartridge.ram.read(stream);

  if(id == ID::SA1ROM) sa1.rom.read(stream);
  if(id == ID::SA1IRAM) sa1.iram.read(stream);
  if(id == ID::SA1BWRAM) sa1.bwram.read(stream);

  if(id == ID::SuperFXROM) superfx.rom.read(stream);
  if(id == ID::SuperFXRAM) superfx.ram.read(stream);

  if(id == ID::ArmDSP) {
    stream.read(armdsp.firmware, stream.size());
  }

  if(id == ID::HitachiDSP) {
    for(unsigned n = 0; n < 1024; n++) hitachidsp.dataROM[n] = stream.readl(3);
  }

  if(id == ID::HitachiDSPROM) hitachidsp.rom.read(stream);

  if(id == ID::Nec7725DSP) {
    for(unsigned n = 0; n <  2048; n++) necdsp.programROM[n] = stream.readl(3);
    for(unsigned n = 0; n <  1024; n++) necdsp.dataROM[n]    = stream.readl(2);
  }

  if(id == ID::Nec96050DSP) {
    for(unsigned n = 0; n < 16384; n++) necdsp.programROM[n] = stream.readl(3);
    for(unsigned n = 0; n <  2048; n++) necdsp.dataROM[n]    = stream.readl(2);
  }

  if(id == ID::NecDSPRAM) {
    for(unsigned n = 0; n < 2048; n++) necdsp.dataRAM[n] = stream.readl(2);
  }

  if(id == ID::EpsonRTC) {
    uint8 data[16] = {0};
    stream.read(data, min(stream.size(), sizeof data));
    epsonrtc.load(data);
  }

  if(id == ID::SharpRTC) {
    uint8 data[16] = {0};
    stream.read(data, min(stream.size(), sizeof data));
    sharprtc.load(data);
  }

  if(id == ID::SPC7110PROM) spc7110.prom.read(stream);
  if(id == ID::SPC7110DROM) spc7110.drom.read(stream);
  if(id == ID::SPC7110RAM) spc7110.ram.read(stream);

  if(id == ID::SDD1ROM) sdd1.rom.read(stream);
  if(id == ID::SDD1RAM) sdd1.ram.read(stream);

  if(id == ID::OBC1RAM) obc1.ram.read(stream);

  if(id == ID::SuperGameBoyBootROM) {
    stream.read(GameBoy::system.bootROM.sgb, min(stream.size(), 256u));
  }

  if(id == ID::SuperGameBoyROM) {
    stream.read(GameBoy::cartridge.romdata, min(GameBoy::cartridge.romsize, stream.size()));
  }

  if(id == ID::SuperGameBoyRAM) {
    stream.read(GameBoy::cartridge.ramdata, min(GameBoy::cartridge.ramsize, stream.size()));
  }

  if(id == ID::BsxFlashROM) bsxflash.memory.read(stream);
  if(id == ID::BsxROM) bsxcartridge.rom.read(stream);
  if(id == ID::BsxRAM) bsxcartridge.ram.read(stream);
  if(id == ID::BsxPSRAM) bsxcartridge.psram.read(stream);

  if(id == ID::SufamiTurboSlotAROM) sufamiturbo.slotA.rom.read(stream);
  if(id == ID::SufamiTurboSlotBROM) sufamiturbo.slotB.rom.read(stream);

  if(id == ID::SufamiTurboSlotARAM) sufamiturbo.slotA.ram.read(stream);
  if(id == ID::SufamiTurboSlotBRAM) sufamiturbo.slotB.ram.read(stream);
}

void Interface::save(unsigned id, const stream &stream) {
  if(id == ID::RAM) stream.write(cartridge.ram.data(), cartridge.ram.size());
  if(id == ID::SA1IRAM) stream.write(sa1.iram.data(), sa1.iram.size());
  if(id == ID::SA1BWRAM) stream.write(sa1.bwram.data(), sa1.bwram.size());
  if(id == ID::SuperFXRAM) stream.write(superfx.ram.data(), superfx.ram.size());

  if(id == ID::NecDSPRAM) {
    for(unsigned n = 0; n < 2048; n++) stream.writel(necdsp.dataRAM[n], 2);
  }

  if(id == ID::EpsonRTC) {
    uint8 data[16] = {0};
    epsonrtc.save(data);
    stream.write(data, sizeof data);
  }

  if(id == ID::SharpRTC) {
    uint8 data[16] = {0};
    sharprtc.save(data);
    stream.write(data, sizeof data);
  }

  if(id == ID::SPC7110RAM) stream.write(spc7110.ram.data(), spc7110.ram.size());
  if(id == ID::SDD1RAM) stream.write(sdd1.ram.data(), sdd1.ram.size());
  if(id == ID::OBC1RAM) stream.write(obc1.ram.data(), obc1.ram.size());

  if(id == ID::SuperGameBoyRAM) stream.write(GameBoy::cartridge.ramdata, GameBoy::cartridge.ramsize);

  if(id == ID::BsxRAM) stream.write(bsxcartridge.ram.data(), bsxcartridge.ram.size());
  if(id == ID::BsxPSRAM) stream.write(bsxcartridge.psram.data(), bsxcartridge.psram.size());

  if(id == ID::SufamiTurboSlotARAM) stream.write(sufamiturbo.slotA.ram.data(), sufamiturbo.slotA.ram.size());
  if(id == ID::SufamiTurboSlotBRAM) stream.write(sufamiturbo.slotB.ram.data(), sufamiturbo.slotB.ram.size());
}

void Interface::unload() {
  save();
  cartridge.unload();
  tracerEnable(false);
}

void Interface::connect(unsigned port, unsigned device) {
  input.connect(port, (Input::Device)device);
}

void Interface::power() {
  system.power();
}

void Interface::reset() {
  system.reset();
}

void Interface::run() {
  system.run();
}

bool Interface::rtc() {
  if(cartridge.has_epsonrtc()) return true;
  if(cartridge.has_sharprtc()) return true;
  return false;
}

void Interface::rtcsync() {
  if(cartridge.has_epsonrtc()) epsonrtc.sync();
  if(cartridge.has_sharprtc()) sharprtc.sync();
}

serializer Interface::serialize() {
  system.runtosave();
  return system.serialize();
}

bool Interface::unserialize(serializer &s) {
  return system.unserialize(s);
}

void Interface::cheatSet(const lstring &list) {
  //Super Game Boy
  if(cartridge.has_gb_slot()) {
    GameBoy::cheat.reset();
    for(auto &code : list) {
      lstring codelist = code.split("+");
      for(auto &part : codelist) {
        unsigned addr, data, comp;
        part.trim();
        if(GameBoy::Cheat::decode(part, addr, data, comp)) GameBoy::cheat.append({addr, data, comp});
      }
    }
    GameBoy::cheat.synchronize();
    return;
  }

  //Super Famicom, Broadcast Satellaview, Sufami Turbo
  cheat.reset();
  for(auto &code : list) {
    lstring codelist = code.split("+");
    for(auto &part : codelist) {
      unsigned addr, data;
      part.trim();
      if(Cheat::decode(part, addr, data)) cheat.append({addr, data});
    }
  }
  cheat.synchronize();
}

void Interface::paletteUpdate() {
  video.generate_palette();
}

bool Interface::tracerEnable(bool trace) {
  string pathname = {path(group(ID::ROM)), "debug/"};
  if(trace == true) directory::create(pathname);

  if(trace == true && !tracer.open()) {
    for(unsigned n = 0; n <= 999; n++) {
      string filename = {pathname, "trace-", decimal<3, '0'>(n), ".log"};
      if(file::exists(filename)) continue;
      tracer.open(filename, file::mode::write);
      return true;
    }
  }

  if(trace == false && tracer.open()) {
    tracer.close();
    return true;
  }

  return false;
}

void Interface::exportMemory() {
  string pathname = {path(group(ID::ROM)), "debug/"};
  directory::create(pathname);

  file::write({pathname, "wram.rwm"}, cpu.wram, 128 * 1024);
  file::write({pathname, "vram.rwm"}, ppu.vram, 64 * 1024);
  file::write({pathname, "oam.rwm"}, ppu.oam, 544);
  file::write({pathname, "cgram.rwm"}, ppu.cgram, 512);
  file::write({pathname, "apuram.rwm"}, smp.apuram, 64 * 1024);
}

Interface::Interface() {
  interface = this;
  system.init();

  information.name        = "Super Famicom";
  information.width       = 256;
  information.height      = 240;
  information.overscan    = true;
  information.aspectRatio = 8.0 / 7.0;
  information.resettable  = true;
  information.capability.states = true;
  information.capability.cheats = true;

  media.append({ID::SuperFamicom, "Super Famicom", "sfc"});
  media.append({ID::SuperFamicom, "Super Famicom", "gb", "Super Game Boy.sfc"});
  media.append({ID::SuperFamicom, "Super Famicom", "bs", "BS-X Satellaview.sfc"});
  media.append({ID::SuperFamicom, "Super Famicom", "st", "Sufami Turbo.sfc"});

  {
    Device device{0, ID::Port1 | ID::Port2, "Controller"};
    device.input.append({ 0, 0, "B"     });
    device.input.append({ 1, 0, "Y"     });
    device.input.append({ 2, 0, "Select"});
    device.input.append({ 3, 0, "Start" });
    device.input.append({ 4, 0, "Up"    });
    device.input.append({ 5, 0, "Down"  });
    device.input.append({ 6, 0, "Left"  });
    device.input.append({ 7, 0, "Right" });
    device.input.append({ 8, 0, "A"     });
    device.input.append({ 9, 0, "X"     });
    device.input.append({10, 0, "L"     });
    device.input.append({11, 0, "R"     });
    device.order = {4, 5, 6, 7, 0, 8, 1, 9, 10, 11, 2, 3};
    this->device.append(device);
  }

  {
    Device device{1, ID::Port1 | ID::Port2, "Multitap"};
    for(unsigned p = 1, n = 0; p <= 4; p++, n += 12) {
      device.input.append({n +  0, 0, {"Port ", p, " - ", "B"     }});
      device.input.append({n +  1, 0, {"Port ", p, " - ", "Y"     }});
      device.input.append({n +  2, 0, {"Port ", p, " - ", "Select"}});
      device.input.append({n +  3, 0, {"Port ", p, " - ", "Start" }});
      device.input.append({n +  4, 0, {"Port ", p, " - ", "Up"    }});
      device.input.append({n +  5, 0, {"Port ", p, " - ", "Down"  }});
      device.input.append({n +  6, 0, {"Port ", p, " - ", "Left"  }});
      device.input.append({n +  7, 0, {"Port ", p, " - ", "Right" }});
      device.input.append({n +  8, 0, {"Port ", p, " - ", "A"     }});
      device.input.append({n +  9, 0, {"Port ", p, " - ", "X"     }});
      device.input.append({n + 10, 0, {"Port ", p, " - ", "L"     }});
      device.input.append({n + 11, 0, {"Port ", p, " - ", "R"     }});
      device.order.append(n + 4, n + 5, n +  6, n +  7, n + 0, n + 8);
      device.order.append(n + 1, n + 9, n + 10, n + 11, n + 2, n + 3);
    }
    this->device.append(device);
  }

  {
    Device device{2, ID::Port1 | ID::Port2, "Mouse"};
    device.input.append({0, 1, "X-axis"});
    device.input.append({1, 1, "Y-axis"});
    device.input.append({2, 0, "Left"  });
    device.input.append({3, 0, "Right" });
    device.order = {0, 1, 2, 3};
    this->device.append(device);
  }

  {
    Device device{3, ID::Port2, "Super Scope"};
    device.input.append({0, 1, "X-axis" });
    device.input.append({1, 1, "Y-axis" });
    device.input.append({2, 0, "Trigger"});
    device.input.append({3, 0, "Cursor" });
    device.input.append({4, 0, "Turbo"  });
    device.input.append({5, 0, "Pause"  });
    device.order = {0, 1, 2, 3, 4, 5};
    this->device.append(device);
  }

  {
    Device device{4, ID::Port2, "Justifier"};
    device.input.append({0, 1, "X-axis" });
    device.input.append({1, 1, "Y-axis" });
    device.input.append({2, 0, "Trigger"});
    device.input.append({3, 0, "Start"  });
    device.order = {0, 1, 2, 3};
    this->device.append(device);
  }

  {
    Device device{5, ID::Port2, "Justifiers"};
    device.input.append({0, 1, "Port 1 - X-axis" });
    device.input.append({1, 1, "Port 1 - Y-axis" });
    device.input.append({2, 0, "Port 1 - Trigger"});
    device.input.append({3, 0, "Port 1 - Start"  });
    device.order.append(0, 1, 2, 3);
    device.input.append({4, 1, "Port 2 - X-axis" });
    device.input.append({5, 1, "Port 2 - Y-axis" });
    device.input.append({6, 0, "Port 2 - Trigger"});
    device.input.append({7, 0, "Port 2 - Start"  });
    device.order.append(4, 5, 6, 7);
    this->device.append(device);
  }

  {
    Device device{6, ID::Port1, "Serial USART"};
    this->device.append(device);
  }

  {
    Device device{7, ID::Port1 | ID::Port2, "None"};
    this->device.append(device);
  }

  port.append({0, "Port 1"});
  port.append({1, "Port 2"});

  for(auto &device : this->device) {
    for(auto &port : this->port) {
      if(device.portmask & (1 << port.id)) {
        port.device.append(device);
      }
    }
  }
}

}
