string Utility::selectCartridge() {
  audio.clear();
  QString filename = QFileDialog::getOpenFileName(0,
    "Load Cartridge",
    utf8() << (config.path.rom != "" ? config.path.rom : config.path.current),
    "SNES images (*.smc *.sfc *.swc *.fig *.bs *.st"
    #if defined(GZIP_SUPPORT)
    " *.zip *.gz"
    #endif
    #if defined(JMA_SUPPORT)
    " *.jma"
    #endif
    ");;"
    "All files (*)"
  );

  string name = filename.toUtf8().constData();
  if(strlen(name) > 0) config.path.current = basepath(name);
  return name;
}

string Utility::selectFolder(const char *title) {
  audio.clear();
  QString pathname = QFileDialog::getExistingDirectory(0,
    title, utf8() << config.path.current,
    QFileDialog::ShowDirsOnly);
  string path = pathname.toUtf8().constData();
  strtr(path, "\\", "/");
  if(strend(path, "/") == false) path << "/";
  return path;
}

void Utility::loadCartridge(const char *filename) {
  SNES::MappedRAM memory;
  if(loadCartridge(filename, memory) == false) return;
  SNES::Cartridge::Type type = SNES::cartridge.detect_image_type(memory.data(), memory.size());
  memory.reset();

  switch(type) {
    case SNES::Cartridge::TypeNormal:           loadCartridgeNormal(filename);                                     break;
    case SNES::Cartridge::TypeBsxSlotted:       winLoader->loadBsxSlottedCartridge(filename, "");                  break;
    case SNES::Cartridge::TypeBsxBios:          winLoader->loadBsxCartridge(filename, "");                         break;
    case SNES::Cartridge::TypeBsx:              winLoader->loadBsxCartridge(config.path.bsx, filename);            break;
    case SNES::Cartridge::TypeSufamiTurboBios:  winLoader->loadSufamiTurboCartridge(filename, "", "");             break;
    case SNES::Cartridge::TypeSufamiTurbo:      winLoader->loadSufamiTurboCartridge(config.path.st, filename, ""); break;
    case SNES::Cartridge::TypeSuperGameBoyBios: winLoader->loadSuperGameBoyCartridge(filename, "");                break;
    case SNES::Cartridge::TypeGameBoy:          winLoader->loadSuperGameBoyCartridge(config.path.sgb, filename);   break;
  }
}

bool Utility::loadCartridgeNormal(const char *base) {
  unloadCartridge();
  if(loadCartridge(cartridge.baseName = base, SNES::memory::cartrom) == false) return false;
  SNES::cartridge.load(SNES::Cartridge::ModeNormal);

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

  cartridge.name = basename(base);

  modifySystemState(LoadCartridge);
  return true;
}

bool Utility::loadCartridgeBsxSlotted(const char *base, const char *slot) {
  unloadCartridge();
  if(loadCartridge(cartridge.baseName = base, SNES::memory::cartrom) == false) return false;
  loadCartridge(cartridge.slotAName = slot, SNES::memory::bsxflash);
  SNES::cartridge.load(SNES::Cartridge::ModeBsxSlotted);

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

  cartridge.name = basename(base);
  if(*slot) cartridge.name << " + " << basename(slot);

  modifySystemState(LoadCartridge);
  return true;
}

bool Utility::loadCartridgeBsx(const char *base, const char *slot) {
  unloadCartridge();
  if(loadCartridge(cartridge.baseName = base, SNES::memory::cartrom) == false) return false;
  loadCartridge(cartridge.slotAName = slot, SNES::memory::bsxflash);
  SNES::cartridge.load(SNES::Cartridge::ModeBsx);

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

  cartridge.name = (*slot ? basename(slot) : basename(base));

  modifySystemState(LoadCartridge);
  return true;
}

bool Utility::loadCartridgeSufamiTurbo(const char *base, const char *slotA, const char *slotB) {
  unloadCartridge();
  if(loadCartridge(cartridge.baseName = base, SNES::memory::cartrom) == false) return false;
  loadCartridge(cartridge.slotAName = slotA, SNES::memory::stArom);
  loadCartridge(cartridge.slotBName = slotB, SNES::memory::stBrom);
  SNES::cartridge.load(SNES::Cartridge::ModeSufamiTurbo);

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

  if(!*slotA && !*slotB) cartridge.name = basename(base);
  else if(!*slotB) cartridge.name = basename(slotA);
  else if(!*slotA) cartridge.name = basename(slotB);
  else cartridge.name = string() << basename(slotA) << " + " << basename(slotB);

  modifySystemState(LoadCartridge);
  return true;
}

bool Utility::loadCartridgeSuperGameBoy(const char *base, const char *slot) {
  unloadCartridge();
  if(loadCartridge(cartridge.baseName = base, SNES::memory::cartrom) == false) return false;
  loadCartridge(cartridge.slotAName = slot, SNES::memory::gbrom);
  SNES::cartridge.load(SNES::Cartridge::ModeSuperGameBoy);

  loadMemory(cartridge.slotAName, ".sav", SNES::memory::gbram);

  cartridge.name = (*slot ? basename(slot) : basename(base));

  modifySystemState(LoadCartridge);
  return true;
}

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

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

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

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

    case SNES::Cartridge::ModeSuperGameBoy: {
      saveMemory(cartridge.slotAName, ".sav", SNES::memory::gbram);
    } break;
  }
}

void Utility::unloadCartridge() {
  if(SNES::cartridge.loaded() == false) return;
  saveMemory();
  modifySystemState(UnloadCartridge);
}

void Utility::modifySystemState(system_state_t state) {
  video.clear();
  audio.clear();

  switch(state) {
    case LoadCartridge: {
      //must call cartridge.load_cart_...() before calling modifySystemState(LoadCartridge)
      if(SNES::cartridge.loaded() == false) break;
      loadCheats();

      application.power = true;
      application.pause = false;
      SNES::system.power();

      //warn if unsupported hardware detected
      string chip;
      if(SNES::cartridge.has_superfx())    chip = "SuperFX";
      else if(SNES::cartridge.has_st011()) chip = "ST011";
      else if(SNES::cartridge.has_st018()) chip = "ST018";
      else if(SNES::cartridge.has_dsp3())  chip = "DSP-3";  //unplayable; only partially supported
      if(chip != "") {
        QMessageBox::warning(winMain->window, "Warning", utf8()
        << "<p><b>Warning:</b><br>Unsupported " << chip << " chip detected. "
        << "It is unlikely that this title will work properly.</p>");
      }

      showMessage(utf8()
      << "Loaded " << cartridge.name
      << (false ? ", and applied UPS patch." : "."));
      winMain->window->setWindowTitle(utf8() << BSNES_TITLE << " - " << cartridge.name);
    } break;

    case UnloadCartridge: {
      if(SNES::cartridge.loaded() == false) break;  //no cart to unload?
      saveCheats();

      SNES::cartridge.unload();

      application.power = false;
      application.pause = true;

      showMessage(utf8() << "Unloaded " << cartridge.name << ".");
      winMain->window->setWindowTitle(utf8() << BSNES_TITLE);
    } break;

    case PowerOn: {
      if(SNES::cartridge.loaded() == false || application.power == true) break;

      application.power = true;
      application.pause = false;
      SNES::system.power();

      showMessage("Power on.");
    } break;

    case PowerOff: {
      if(SNES::cartridge.loaded() == false || application.power == false) break;

      application.power = false;
      application.pause = true;

      showMessage("Power off.");
    } break;

    case PowerCycle: {
      if(SNES::cartridge.loaded() == false) break;

      application.power = true;
      application.pause = false;
      SNES::system.power();

      showMessage("System power was cycled.");
    } break;

    case Reset: {
      if(SNES::cartridge.loaded() == false || application.power == false) break;

      application.pause = false;
      SNES::system.reset();

      showMessage("System was reset.");
    } break;
  }

  winMain->syncUi();
  winCodeEditor->dismiss();
  winCheatEditor->reloadList();
  winCheatEditor->syncUi();
}
//

bool Utility::loadCartridge(const char *filename, SNES::MappedRAM &memory) {
  if(file::exists(filename) == false) return false;
  Reader::Type filetype = Reader::detect(filename, config.file.autodetect_type);

  uint8_t *data;
  unsigned size;

  switch(filetype) { default:
    case Reader::Normal: {
      FileReader fp(filename);
      if(!fp.ready()) return false;
      size = fp.size();
      data = fp.read();
    } break;

    #ifdef GZIP_SUPPORT
    case Reader::GZIP: {
      GZReader fp(filename);
      if(!fp.ready()) return false;
      size = fp.size();
      data = fp.read();
    } break;

    case Reader::ZIP: {
      ZipReader fp(filename);
      if(!fp.ready()) return false;
      size = fp.size();
      data = fp.read();
    } break;
    #endif

    #ifdef JMA_SUPPORT
    case Reader::JMA: {
      try {
        JMAReader fp(filename);
        size = fp.size();
        data = fp.read();
      } catch(JMA::jma_errors) {
        return false;
      }
    } break;
    #endif
  }

  if((size & 0x7fff) == 512) memmove(data, data + 512, size -= 512);
  memory.map(data, size);
  return true;
}

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

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

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

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

  memcpy(memory.data(), data, min(size, memory.size()));
  return true;
}

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

  string name;
  name << filepath(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;
}

void Utility::loadCheats() {
  string name, data;
  name << filepath(basename(cartridge.baseName), config.path.cheat);
  name << ".cht";

  SNES::cheat.clear();
  if(data.readfile(name)) SNES::cheat.load(data);
}

void Utility::saveCheats() {
  string name;
  name << filepath(basename(cartridge.baseName), config.path.cheat);
  name << ".cht";

  file fp;
  if(SNES::cheat.count() > 0 || file::exists(name)) {
    if(fp.open(name, file::mode_write)) {
      fp.print(SNES::cheat.save());
      fp.close();
    }
  }
}

//

//ensure file path is absolute (eg resolve relative paths)
string Utility::filepath(const char *filename, const char *pathname) {
  //if no pathname, return filename as-is
  string file(filename);
  file.replace("\\", "/");

  string path = (!pathname || !*pathname) ? (const char*)config.path.current : pathname;
  //ensure path ends with trailing '/'
  path.replace("\\", "/");
  if(!strend(path, "/")) path.append("/");

  //replace relative path with absolute path
  if(strbegin(path, "./")) {
    ltrim(path, "./");
    path = string() << config.path.base << path;
  }

  //remove folder part of filename
  lstring part;
  part.split("/", file);
  return path << part[part.size() - 1];
}

//remove directory information and file extension ("/foo/bar.ext" -> "bar")
string Utility::basename(const char *filename) {
  string name(filename);

  //remove extension
  for(signed i = strlen(name) - 1; i >= 0; i--) {
    if(name[i] == '.') {
      name[i] = 0;
      break;
    }
  }

  //remove directory information
  for(signed i = strlen(name) - 1; i >= 0; i--) {
    if(name[i] == '/' || name[i] == '\\') {
      i++;
      char *output = name();
      while(true) {
        *output++ = name[i];
        if(!name[i]) break;
        i++;
      }
      break;
    }
  }

  return name;
}

//remove filename and return path only ("/foo/bar.ext" -> "/foo/")
string Utility::basepath(const char *filename) {
  string path(filename);
  path.replace("\\", "/");

  //remove filename
  for(signed i = strlen(path) - 1; i >= 0; i--) {
    if(path[i] == '/') {
      path[i] = 0;
      break;
    }
  }

  if(!strend(path, "/")) path.append("/");
  return path;
}

