void InterfaceSNES::initialize() {
  SNES::interface = this;
  SNES::system.init();
}

void InterfaceSNES::setController(bool port, unsigned device) {
  if(port == 0) config->snes.controllerPort1Device = device;
  if(port == 1) config->snes.controllerPort2Device = device;

  if(port == 0) switch(device) {
  case 0: return SNES::input.connect(0, SNES::Input::Device::None);
  case 1: return SNES::input.connect(0, SNES::Input::Device::Joypad);
  case 2: return SNES::input.connect(0, SNES::Input::Device::Multitap);
  case 3: return SNES::input.connect(0, SNES::Input::Device::Mouse);
  }

  if(port == 1) switch(device) {
  case 0: return SNES::input.connect(1, SNES::Input::Device::None);
  case 1: return SNES::input.connect(1, SNES::Input::Device::Joypad);
  case 2: return SNES::input.connect(1, SNES::Input::Device::Multitap);
  case 3: return SNES::input.connect(1, SNES::Input::Device::Mouse);
  case 4: return SNES::input.connect(1, SNES::Input::Device::SuperScope);
  case 5: return SNES::input.connect(1, SNES::Input::Device::Justifier);
  case 6: return SNES::input.connect(1, SNES::Input::Device::Justifiers);
  case 7: return SNES::input.connect(1, SNES::Input::Device::Serial);
  }
}

bool InterfaceSNES::cartridgeLoaded() {
  return SNES::cartridge.loaded();
}

bool InterfaceSNES::loadCartridge(const string &basename) {
  uint8_t *data;
  unsigned size;
  if(interface->loadFile(basename, data, size) == false) return false;

  interface->unloadCartridge();
  interface->baseName = nall::basename(basename);
  interface->slotName = { nall::basename(basename) };

  string markup;
  markup.readfile({ interface->baseName, ".xml" });
  if(markup == "") markup = SnesCartridge(data, size).markup;

  SNES::cartridge.rom.copy(data, size);
  SNES::cartridge.load(SNES::Cartridge::Mode::Normal, markup);
  SNES::system.power();

  delete[] data;

  loadMemory();
  interface->loadCartridge(::Interface::Mode::SNES);
  SNES::video.generate(SNES::Video::Format::RGB30);
  return true;
}

bool InterfaceSNES::loadSatellaviewSlottedCartridge(const string &basename, const string &slotname) {
  uint8_t *data[2];
  unsigned size[2];
  if(interface->loadFile(basename, data[0], size[0]) == false) return false;
  interface->loadFile(slotname, data[1], size[1]);

  interface->unloadCartridge();
  interface->baseName = nall::basename(basename);
  if(data[1]) interface->baseName.append("+", nall::basename(notdir(slotname)));
  interface->slotName = { nall::basename(basename), nall::basename(slotname) };

  string markup;
  markup.readfile({ interface->baseName, ".xml" });
  if(markup == "") markup = SnesCartridge(data[0], size[0]).markup;

  SNES::cartridge.rom.copy(data[0], size[0]);
  if(data[1]) SNES::bsxflash.memory.copy(data[1], size[1]);
  SNES::cartridge.load(SNES::Cartridge::Mode::BsxSlotted, markup);
  SNES::system.power();

  delete[] data[0];
  if(data[1]) delete[] data[1];

  loadMemory();
  interface->loadCartridge(::Interface::Mode::SNES);
  SNES::video.generate(SNES::Video::Format::RGB30);
  return true;
}

bool InterfaceSNES::loadSatellaviewCartridge(const string &basename, const string &slotname) {
  uint8_t *data[2];
  unsigned size[2];
  if(interface->loadFile(basename, data[0], size[0]) == false) return false;
  interface->loadFile(slotname, data[1], size[1]);

  interface->unloadCartridge();
  interface->baseName = nall::basename(basename);
  if(data[1]) interface->baseName.append("+", nall::basename(notdir(slotname)));
  interface->slotName = { nall::basename(basename), nall::basename(slotname) };

  string markup;
  markup.readfile({ interface->baseName, ".xml" });
  if(markup == "") markup = SnesCartridge(data[0], size[0]).markup;

  SNES::cartridge.rom.copy(data[0], size[0]);
  if(data[1]) SNES::bsxflash.memory.copy(data[1], size[1]);
  SNES::cartridge.load(SNES::Cartridge::Mode::Bsx, markup);
  SNES::system.power();

  delete[] data[0];
  if(data[1]) delete[] data[1];

  loadMemory();
  interface->loadCartridge(::Interface::Mode::SNES);
  SNES::video.generate(SNES::Video::Format::RGB30);
  return true;
}

bool InterfaceSNES::loadSufamiTurboCartridge(const string &basename, const string &slotAname, const string &slotBname) {
  uint8_t *data[3];
  unsigned size[3];
  if(interface->loadFile(basename, data[0], size[0]) == false) return false;
  interface->loadFile(slotAname, data[1], size[1]);
  interface->loadFile(slotBname, data[2], size[2]);

  interface->unloadCartridge();
  interface->baseName = nall::basename(basename);
  if(data[1] && data[2]) interface->baseName = { nall::basename(slotAname), "+", nall::basename(notdir(slotBname)) };
  else if(data[1]) interface->baseName = nall::basename(slotAname);
  else if(data[2]) interface->baseName = nall::basename(slotBname);
  interface->slotName = { nall::basename(basename), nall::basename(slotAname), nall::basename(slotBname) };

  string markup;
  markup.readfile({ interface->baseName, ".xml" });
  if(markup == "") markup = SnesCartridge(data[0], size[0]).markup;

  SNES::cartridge.rom.copy(data[0], size[0]);
  if(data[1]) SNES::sufamiturbo.slotA.rom.copy(data[1], size[1]);
  if(data[2]) SNES::sufamiturbo.slotB.rom.copy(data[1], size[1]);
  SNES::cartridge.load(SNES::Cartridge::Mode::SufamiTurbo, markup);
  SNES::system.power();

  delete[] data[0];
  if(data[1]) delete[] data[1];
  if(data[2]) delete[] data[2];

  loadMemory();
  interface->loadCartridge(::Interface::Mode::SNES);
  SNES::video.generate(SNES::Video::Format::RGB30);
  return true;
}

bool InterfaceSNES::loadSuperGameBoyCartridge(const string &basename, const string &slotname) {
  uint8_t *data[2];
  unsigned size[2];
  if(interface->loadFile(basename, data[0], size[0]) == false) return false;
  interface->loadFile(slotname, data[1], size[1]);

  interface->unloadCartridge();
  interface->baseName = nall::basename(basename);
  if(data[1]) interface->baseName = nall::basename(slotname);
  interface->slotName = { nall::basename(basename), nall::basename(slotname) };

  string markup;
  markup.readfile({ interface->baseName, ".xml" });
  if(markup == "") markup = SnesCartridge(data[0], size[0]).markup;

  string gbMarkup;
  gbMarkup.readfile({ nall::basename(slotname), ".xml" });
  if(gbMarkup == "") gbMarkup = GameBoyCartridge(data[1], size[1]).markup;

  SNES::cartridge.rom.copy(data[0], size[0]);
  GameBoy::cartridge.load(GameBoy::System::Revision::SuperGameBoy, gbMarkup, data[1], size[1]);
  SNES::cartridge.load(SNES::Cartridge::Mode::SuperGameBoy, markup);
  SNES::system.power();

  delete[] data[0];
  if(data[1]) delete[] data[1];

  loadMemory();
  interface->loadCartridge(::Interface::Mode::SNES);
  SNES::video.generate(SNES::Video::Format::RGB30);
  return true;
}

void InterfaceSNES::unloadCartridge() {
  saveMemory();
  SNES::cartridge.unload();
  interface->baseName = "";
}

void InterfaceSNES::power() {
  SNES::system.power();
}

void InterfaceSNES::reset() {
  SNES::system.reset();
}

void InterfaceSNES::run() {
  SNES::system.run();
}

//slot[] array = Cartridge::Slot to slot# conversion:
//{ Base, Bsx, SufamiTurbo, SufamiTurboA, SufamiTurboB, GameBoy }

void InterfaceSNES::loadMemory() {
  static unsigned slot[] = { 0, 0, 0, 1, 2, 1 };
  for(auto &memory : SNES::cartridge.nvram) {
    if(memory.size == 0) continue;
    string filename = { interface->slotName[slot[(unsigned)memory.slot]], memory.id };
    uint8_t *data;
    unsigned size;
    if(file::read(filename, data, size)) {
      memcpy(memory.data, data, min(memory.size, size));
      delete[] data;
    }
  }
}

void InterfaceSNES::saveMemory() {
  static unsigned slot[] = { 0, 0, 0, 1, 2, 1 };
  for(auto &memory : SNES::cartridge.nvram) {
    if(memory.size == 0) continue;
    string filename = { interface->slotName[slot[(unsigned)memory.slot]], memory.id };
    file::write(filename, memory.data, memory.size);
  }
}

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

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

void InterfaceSNES::setCheats(const lstring &list) {
  if(SNES::cartridge.mode() == SNES::Cartridge::Mode::SuperGameBoy) {
    GameBoy::cheat.reset();
    for(auto &code : list) {
      lstring codelist;
      codelist.split("+", code);
      for(auto &part : codelist) {
        unsigned addr, data, comp;
        if(GameBoy::Cheat::decode(part, addr, data, comp)) {
          GameBoy::cheat.append({ addr, data, comp });
        }
      }
    }
    GameBoy::cheat.synchronize();
    return;
  }

  SNES::cheat.reset();
  for(auto &code : list) {
    lstring codelist;
    codelist.split("+", code);
    for(auto &part : codelist) {
      unsigned addr, data;
      if(SNES::Cheat::decode(part, addr, data)) {
        SNES::cheat.append({ addr, data });
      }
    }
  }
  SNES::cheat.synchronize();
}

//

void InterfaceSNES::videoRefresh(const uint32_t *data, bool hires, bool interlace, bool overscan) {
  static uint32_t output[512 * 480];

  unsigned width = 256 << hires;
  unsigned height = 240 << interlace;
  unsigned pitch = 1024 >> interlace;

  //skip first line; as it is always blank (by SNES design)
  if(overscan == false) data +=  1 * 1024;  // 8 + 224 +  8
  if(overscan == true ) data +=  9 * 1024;  // 0 + 240 +  0

  for(unsigned y = 0; y < height; y++) {
    const uint32_t *sp = data + y * pitch;
    uint32_t *dp = output + y * 512;
    for(unsigned x = 0; x < width; x++) {
      *dp++ = SNES::video.palette[*sp++];
    }
  }

  if(config->video.maskOverscan) {
    unsigned osw = config->video.maskOverscanHorizontal << hires;
    unsigned osh = config->video.maskOverscanVertical << interlace;

    for(unsigned y = 0; y < height; y++) {
      uint32_t *dp = output + y * 512;
      if(y < osh || y >= height - osh) {
        memset(dp, 0, width * sizeof(uint32_t));
      } else {
        memset(dp + 0, 0, osw * sizeof(uint32_t));
        memset(dp + width - osw, 0, osw * sizeof(uint32_t));
      }
    }
  }

  interface->videoRefresh(output, 512 * sizeof(uint32_t), width, height);
}

void InterfaceSNES::audioSample(int16_t lsample, int16_t rsample) {
  signed samples[] = { lsample, rsample };
  dspaudio.sample(samples);
  while(dspaudio.pending()) {
    dspaudio.read(samples);
    audio.sample(samples[0], samples[1]);
  }
}

int16_t InterfaceSNES::inputPoll(bool port, SNES::Input::Device device, unsigned index, unsigned id) {
  if(port == 0) {
    if(device == SNES::Input::Device::Joypad) return inputManager->snes.port1.gamepad.poll(id);
    if(device == SNES::Input::Device::Multitap) {
      if(index == 0) return inputManager->snes.port1.multitap1.poll(id);
      if(index == 1) return inputManager->snes.port1.multitap1.poll(id);
      if(index == 2) return inputManager->snes.port1.multitap1.poll(id);
      if(index == 3) return inputManager->snes.port1.multitap1.poll(id);
    }
    if(device == SNES::Input::Device::Mouse) return inputManager->snes.port1.mouse.poll(id);
  }

  if(port == 1) {
    if(device == SNES::Input::Device::Joypad) return inputManager->snes.port2.gamepad.poll(id);
    if(device == SNES::Input::Device::Multitap) {
      if(index == 0) return inputManager->snes.port2.multitap1.poll(id);
      if(index == 1) return inputManager->snes.port2.multitap1.poll(id);
      if(index == 2) return inputManager->snes.port2.multitap1.poll(id);
      if(index == 3) return inputManager->snes.port2.multitap1.poll(id);
    }
    if(device == SNES::Input::Device::Mouse) return inputManager->snes.port2.mouse.poll(id);
    if(device == SNES::Input::Device::SuperScope) return inputManager->snes.port2.superScope.poll(id);
    if(device == SNES::Input::Device::Justifier) return inputManager->snes.port2.justifier1.poll(id);
    if(device == SNES::Input::Device::Justifiers) {
      if(index == 0) return inputManager->snes.port2.justifier1.poll(id);
      if(index == 1) return inputManager->snes.port2.justifier2.poll(id);
    }
  }

  return 0;
}

string InterfaceSNES::path(SNES::Cartridge::Slot slot, const string &hint) {
  static unsigned index[] = { 0, 0, 0, 1, 2, 1 };
  return { interface->slotName[index[(unsigned)slot]], hint };
}

void InterfaceSNES::message(const string &text) {
  MessageWindow::information(*mainWindow, text);
}
