/*!
 * @file iso_cd.cpp
 * IsoFs API for accessing the CD/DVD drive.
 */

#include "iso_cd.h"

#include <cstring>

#include "isocommon.h"
#include "overlord.h"
#include "soundcommon.h"
#include "srpc.h"

#include "common/log/log.h"

#include "game/sce/iop.h"
#include "game/sce/stubs.h"

// iso_cd is an implementation of the IsoFs API for loading files from a CD/DVD with an ISO and/or
// DUP filesystem.
// The DUP filesystem is a custom Naughty Dog filesystem which attempts to hide
// files. The DUP filesystem also stores all files twice on the disk and will try reading from the
// other copy if it reading the first copy encounters errors.  The DUP filesystem is unused.

using namespace iop;
typedef int (*mmode_func)(int);

// Drive State
// sector to read from (for DUP files, sector of the first copy of the file)
u32 _sector;
// number of sectors to read
u32 _sectors;
// number of retries in the current read
u32 _retries;
// buffer to read into
void* _buffer;
// set to 0 or 1 to indicate if the first or second copy of DUP files should be used.
uint32_t _dupseg;
// the actual sector to read from (differs from _sector when reading second copy of DUP file)
uint32_t _real_sector;
// set 1 if the current read was continuous from the previous read (didn't require a seek)
uint32_t _continuous;
// time when the current read was started
SysClock _starttime;
// time when the current read has ended
SysClock _endtime;

// Globals
u32 gDirtyCd;          // set when we're waiting on a read which has errors
u32 gNoCD;             // set when we believe the game disc has been removed.
static u32 sNumFiles;  // number of files (includes both ISO and DUP files)
static u32 sArea1;     // Sector where the first copy of DUP files live.
static u32 sAreaDiff;  // Sectors in between the first and second copy of files.

u32 pirated;                                // do we think the game is pirated?
mmode_func cdmmode = nullptr;               // function to call to set the expected media (CD/DVD)
static sceCdRMode sNominalMode;             // drive settings for "nominal" reading
static sceCdRMode sStreamMode;              // drive settings for "streaming" reading
static sceCdRMode* sMode;                   // pointer to currently selected read mode
static LoadStackEntry* sReadInfo;           // LoadStackEntry for currently reading file
static u8* sSecBuffer[3];                   // Buffers for a single sector
u32 add_files;                              // Should we add files we discover to the sFiles list?
static FileRecord sFiles[MAX_ISO_FILES];    // Info for all files on the disc
u32 CD_ID_SectorNum;                        // Sector of the DISK.ID file
s32 CD_ID_Sector[SECTOR_SIZE / 4];          // Contents of the DISK.ID file
s32 CD_ID_SectorSum;                        // Sum of the CD_ID_SECTOR array
LoadStackEntry sLoadStack[MAX_OPEN_FILES];  // List of all files that are "open"
static u32 sound_bank_loads;                // might be a static variable in a function?
IsoFs iso_cd_;                              // IsoFs function pointers

constexpr int TIME_SIZE = 16;  // how many samples for read timing
s32 _times[TIME_SIZE];         // read timing data
s32 _timesix;
s32 _tsamps[2];
s32 _tkps[2];

s32 gLastSpeed;
s32 gDiskSpeed[2];
s32 gDupSeg;

u32 ReadU32(u8* buffer);
u32 ReadSectorsNow(uint32_t sector, uint32_t len, void* buffer);
u32 ReadDirectory(uint32_t sector, uint32_t size, uint32_t secBufID);
void DecodeDUP(u8* buffer);
void LoadMusicTweaks(u8* buffer);
void LoadDiscID();
u32 CheckDiscID();
void SetRealSector();
void CD_WaitReturn();

static int FS_Init(u8* buffer);
static FileRecord* FS_Find(const char* name);
static FileRecord* FS_FindIN(const char* iso_name);
static uint32_t FS_GetLength(FileRecord* fr);
static LoadStackEntry* FS_Open(FileRecord* fr, int32_t offset);
static LoadStackEntry* FS_OpenWad(FileRecord* fr, int32_t offset);
static void FS_Close(LoadStackEntry* fd);
static uint32_t FS_BeginRead(LoadStackEntry* fd, void* buffer, int32_t len);
static uint32_t FS_SyncRead();
static uint32_t FS_LoadSoundBank(char*, void*);
static uint32_t FS_LoadMusic(char*, void*);
static void FS_PollDrive();

void iso_cd_init_globals() {
  _sector = 0;
  _sectors = 0;
  _retries = 0;
  gDirtyCd = 0;
  gNoCD = 0;
  _dupseg = 0;
  _real_sector = 0;
  _continuous = 0;
  _buffer = nullptr;
  memset(&_starttime, 0, sizeof(SysClock));
  memset(&_endtime, 0, sizeof(SysClock));

  sNumFiles = 0;
  sArea1 = 0;
  sAreaDiff = 0;

  pirated = 0;
  cdmmode = nullptr;

  sNominalMode.trycount = 0;
  sNominalMode.spindlctrl = 1;
  sNominalMode.datapattern = 0;
  sNominalMode.pad = 0;

  sStreamMode.trycount = 0xf;
  sStreamMode.spindlctrl = 0;
  sStreamMode.datapattern = 0;
  sStreamMode.pad = 0;

  sMode = &sStreamMode;
  sReadInfo = nullptr;

  memset(sSecBuffer, 0, sizeof(sSecBuffer));
  add_files = 0;
  memset(sFiles, 0, sizeof(sFiles));

  CD_ID_SectorNum = 0;
  memset(CD_ID_Sector, 0, sizeof(CD_ID_Sector));
  CD_ID_SectorSum = 0;
  memset(sLoadStack, 0, sizeof(sLoadStack));
  sound_bank_loads = 0;

  iso_cd_.init = FS_Init;
  iso_cd_.find = FS_Find;
  iso_cd_.find_in = FS_FindIN;
  iso_cd_.get_length = FS_GetLength;
  iso_cd_.open = FS_Open;
  iso_cd_.open_wad = FS_OpenWad;
  iso_cd_.close = FS_Close;
  iso_cd_.begin_read = FS_BeginRead;
  iso_cd_.sync_read = FS_SyncRead;
  iso_cd_.load_sound_bank = FS_LoadSoundBank;
  iso_cd_.load_music = FS_LoadMusic;
  iso_cd_.poll_drive = FS_PollDrive;

  memset(_times, 0, sizeof(_times));
  memset(_tsamps, 0, sizeof(_tsamps));
  memset(_tkps, 0, sizeof(_tkps));
  _timesix = 0;

  memset(gDiskSpeed, 0, sizeof(gDiskSpeed));
  gLastSpeed = 0;
  gDupSeg = 0;
}

/*!
 * Read unaligned uint32_t from buffer.  ISO file systems may have 32-bit values that aren't word
 * aligned. DONE, EXACT
 */
uint32_t ReadU32(u8* data) {
  return (uint32_t)data[0] + (((uint32_t)data[1]) * 0x100) + (((uint32_t)data[2]) * 0x10000) +
         (((uint32_t)data[3]) * 0x1000000);
}

/*!
 * Read from disc, immediately (blocking), into a local buffer.  Will retry if needed, setting
 * gDirtyCd. This does not use the DUP file system or drive state, so this should not be used
 * outside of initialization. Will clear gDirtyCd on successful read. Returns 1 on success, 0 on
 * sceCdRead failure, or otherwise retries forever until sceCdGetError is OK.
 * The length is in terms of sectors.
 * DONE, EXACT
 */
u32 ReadSectorsNow(uint32_t sector, uint32_t len, void* buffer) {
  // reset the sector state to break any continuous reads in progress
  _sector = 0;

  // retry loop for read
  while (true) {
    // Start async read from DVD...
    if (sceCdRead(sector, len, buffer, sMode) == 0) {
      // if this fails, it indicates catastrophic failure of the DVD drive, so give up immediately.
      return 0;
    }

    // Wait for read to finish (0x00 is blocking)
    sceCdSync(0);

    // check for error
    if (sceCdGetError() == 0) {
      // no error, we are good!
      break;
    }
    // we got an error.  Try again, and set a dirty flag so the EE knows we're having trouble
    gDirtyCd = 1;
  }

  // success! clear the dirty flag and return!
  gDirtyCd = 0;
  return 1;
}

/*!
 * Read ISO file systems directory tree, adding files to the filerecord table if add_files is set.
 * Regardless of add_files is not set, it will be set for folders under NAUGHTY.DOG.
 * This feature is used on demos with multiple games and Jak is in the NAUGHTY.DOG folder.
 * Recursively walks the tree.
 * There is a stack of single sector buffers used to recursively read the directories.
 * Returns 1 on success and 0 on failure.
 * Running out of sector buffers because of too many nested folders is considered success?
 * DONE
 */
u32 ReadDirectory(uint32_t sector, uint32_t size, uint32_t secBufID) {
  if (secBufID < 3) {
    // grab our buffer from the stack
    u8* buffer = sSecBuffer[secBufID];

    uint32_t lsector = sector;
    int32_t lsize = size;

    // loop over sector reads
    while (lsize > 0) {
      // ISO low-level read
      if (!ReadSectorsNow(lsector, 1, buffer)) {
        lg::info("[OVERLORD ISO CD] Failed to read sector in ReadDirectory!");
        return 0;
      }
      u8* lbuffer = buffer;

      // loop over stuff in the sector
      while ((*lbuffer != 0) && (lbuffer < buffer + SECTOR_SIZE)) {
        u8 dir_record_size = *lbuffer;
        if ((lbuffer[0x21] != 0) && (lbuffer[0x21] != 1)) {  // skip over whatever these things are

          uint32_t extent = ReadU32(lbuffer + 2);
          uint32_t dir_size = ReadU32(lbuffer + 10);
          uint32_t name_len = lbuffer[0x20];

          bool is_directory = true;
          if ((lbuffer[0x1f + name_len] == ';') && (lbuffer[0x20 + name_len] == '1')) {
            is_directory = false;
          }

          if (is_directory) {
            if (!add_files) {
              // don't add file by default, but add files if we recurse in the NAUGHTY.DOG folder
              if (!memcmp(lbuffer + 0x21, "NAUGHTY.DOG", 0xb)) {
                add_files = true;
                ReadDirectory(extent, dir_size, secBufID + 1);
                add_files = false;
              }
            } else {
              // otherwise just recurse
              ReadDirectory(extent, dir_size, secBufID + 1);
            }
          } else {
            if (sNumFiles == MAX_ISO_FILES) {
              lg::info("[OVERLORD ISO CD] There are too many files on the disc!");
              return 0;
            }

            if (add_files) {
              lbuffer[0x1f + name_len] = 0;  // null terminate the name
              MakeISOName(sFiles[sNumFiles].name, (char*)(lbuffer + 0x21));
              sFiles[sNumFiles].location = extent;
              sFiles[sNumFiles].size = dir_size;
              sNumFiles++;
            }
          }
        }
        lbuffer += dir_record_size;
      }
      lsector++;
      lsize -= 0x800;
    }
  } else {
    lg::info("[OVERLORD ISO CD] ReadDirectory ran out of sector buffers!");
  }
  return 1;
}

struct DupIndexEntry {
  // 20 bytes
  char name[12];
  u32 location;
  u32 size;
};

/*!
 * DUP code.  The DUP files aren't on the disc, so this doesn't do anything.
 * This would allow for hidden files that aren't in the standard ISO format, presumably to make
 * pirating harder? The DUP files store the location of these hidden files. Also it support having
 * two copies of some files.  There are two areas, both of which are identical. But it was never
 * used.
 */
void DecodeDUP(u8* buffer) {
  (void)buffer;

  // set sArea1 to point to an impossibly large sector - if DUP initialization fails this means no
  // file will be in the DUP zones.
  sArea1 = 0x7fffffff;

  char iso_name[16];
  // all three of these will fail.
  MakeISOName(iso_name, "Z1INDEX.DUP");
  FileRecord* index_file = FS_FindIN(iso_name);
  MakeISOName(iso_name, "Z3AREA1.DUP");
  FileRecord* area1_file = FS_FindIN(iso_name);
  MakeISOName(iso_name, "Z5AREA2.DUP");
  FileRecord* area2_file = FS_FindIN(iso_name);

  // Note - this reads 4 sectors, but the buffer only has enough room for 3 sectors.
  // So this code would likely cause a crash if it was run.
  // Maybe this is why it was removed?
  // Or maybe there used to be 4 init buffers, but one was removed once they gave up on DUP?
  if (index_file && area1_file && area2_file && ReadSectorsNow(index_file->location, 4, buffer)) {
    sArea1 = area1_file->location;                            // marks start of 1st zone
    sAreaDiff = area2_file->location - area1_file->location;  // difference between zones

    // make sure we have enough room to store all entries
    if (sNumFiles + *(s32*)(buffer) <= MAX_ISO_FILES) {
      // read entries
      DupIndexEntry* dup_entries = (DupIndexEntry*)(((u8*)buffer) + 4);
      for (int i = 0; i < *(s32*)(buffer); i++) {
        *(s32*)(&sFiles[sNumFiles].name) = *(s32*)(&dup_entries[i].name);
        *(s32*)(&sFiles[sNumFiles].name + 4) = *(s32*)(&dup_entries[i].name + 4);
        *(s32*)(&sFiles[sNumFiles].name + 8) = *(s32*)(&dup_entries[i].name + 8);
        sFiles[sNumFiles].size = dup_entries[i].size;
        sFiles[sNumFiles].location = dup_entries[i].location;
        sNumFiles++;
      }
    }
  }
}

/*!
 * Load the TWEAKVAL.MUS file into the gMusicTweakInfo file.
 * Only works if the file is less than 1 sector long.
 * If loading fails, writes a 0 to the first 32-bits of gMusicTweakInfo
 * @param buffer a sector buffer which will be used
 */
void LoadMusicTweaks(u8* buffer) {
  char iso_name[16];
  MakeISOName(iso_name, "TWEAKVAL.MUS");
  FileRecord* fr = FS_FindIN(iso_name);
  if (!fr || !ReadSectorsNow(fr->location, 1, buffer)) {
    gMusicTweakInfo.TweakCount = 0;
    lg::warn("[OVERLORD ISO CD] Failed to load music tweaks!");
  } else {
    memcpy((void*)&gMusicTweakInfo, buffer, sizeof(MusicTweaks));
  }
}

/*!
 * Load the DISK ID file and compute the sum.
 * This is used as a checksum to make sure the disc is correct.
 * A literal sum is not a great checksum
 * Also, this function name and the file on the disc itself spell dis{c,k} differently.
 *
 * If there is no DISK_ID.DIZ file, uses whatever is stored at 0x400 instead.
 */
void LoadDiscID() {
  char iso_name[16];
  MakeISOName(iso_name, "DISK_ID.DIZ");
  FileRecord* fr = FS_FindIN(iso_name);
  if (!fr) {
    lg::warn(
        "[OVERLORD ISO CD] LoadDiscID failed to find DISK_ID.DIZ, using sector 0x400 instead!");
    CD_ID_SectorNum = 0x400;
  } else {
    CD_ID_SectorNum = fr->location;
  }

  ReadSectorsNow(CD_ID_SectorNum, 1, &CD_ID_Sector);
  CD_ID_SectorSum = 0;
  for (uint32_t i = 0; i < SECTOR_SIZE / 4; i++) {
    CD_ID_SectorSum += CD_ID_Sector[i];
  }
  lg::info("[OVERLORD] DISK_ID.DIZ OK 0x{:x}\n", CD_ID_SectorSum);
}

/*!
 * Verify that the DISK ID file has not changed. Returns 1 if it is good.
 */
u32 CheckDiskID() {
  if (ReadSectorsNow(CD_ID_SectorNum, 1, CD_ID_Sector) == 0) {
    // failed to read CD ID data
    return 0;
  }

  int sum = 0;
  for (uint32_t i = 0; i < SECTOR_SIZE / 4; i++) {
    sum += CD_ID_Sector[i];
  }
  return sum == CD_ID_SectorSum;
}

/*!
 * Set _real_sector in preparation for a read, based on the requested _sector.
 * This has logic for a system which has a double copy of some data on the disc and can pick between
 * two different copies. This selection is done with the dupseg flag.
 */
void SetRealSector() {
  // if we are below sArea1, it's not a duplicated file, so ignore the dupseg flag and read directly
  if (_sector < sArea1 || _dupseg == 0) {
    _real_sector = _sector;
  } else {
    // it's a duplicated file, and duplicate read is enabled, so get the area 2 sector.
    _real_sector = _sector + sAreaDiff;
    lg::warn("[OVERLORD] Warning, adjusting real sector in SetRealSector");
  }

  // we suspect the game is pirated, load the wrong sector.
  if (pirated) {
    _real_sector += 3;
    lg::warn("Pirated!");
  }
}

/*!
 * Initialize the ISO CD system and builds file record table.
 * This is an ISO_FS API Function.
 * Also loads music tweaks/DISK ID
 * @param buffer : a buffer larger enough to hold 3 sectors
 *                 this buffer can be freed immediately this returns
 * Return 0 on success.
 */
int FS_Init(u8* buffer) {
  // determine disk type
  int disk_type = SCECdDETCT;
  while (disk_type = sceCdGetDiskType(), disk_type == SCECdDETCT) {
    // This SleepThread will cause the Overlord initialization to lock up. It's called with an
    // argument of 10000, but SleepThread accepts no arguments. Probably they meant to call
    // DelayThread. It ends up working because the drive already knows the disk type at this point.
    SleepThread();
  }

  // what is this. it's crazy. why?
  if (disk_type <= SCECdPS2DVD || disk_type < SCECdCDDA || disk_type <= SCECdDVDV ||
      disk_type != SCECdIllegalMedia) {
    // we are actually using the CD drive, so set the mmode function to the SCE function.
    // This is called in FS_LoadMusic.  If you call this with the wrong media type, it locks up.
    // I guess this is an attempt at making convoluted anti-piracy code so it's harder to find
    // calls to sceCdMmode with static analysis.  But they left in debug symbols and the variable
    // is called "cdmmode", which is not a very sneaky way to hide it!  (At least on the EE it's
    // called aybabtu and is a GOAL symbol which is way harder to figure out.)  Also it seems like
    // the primary mode of piracy they were concerned with is somebody swapping a DVD with a CD?
    cdmmode = sceCdMmode;

    // verify the disc is a DVD.
    sceCdMmode(SCECdDVD);

    // set up sector buffers used for initialization reads.
    for (int i = 0; i < 3; i++) {
      sSecBuffer[i] = buffer + i * SECTOR_SIZE;
    }

    // read primary volume descriptor into buffer
    if (!ReadSectorsNow(0x10, 1, sSecBuffer[0])) {
      lg::warn("[OVERLORD ISO CD] Failed to read primary volume descriptor");
      return 1;
    }

    // check volume descriptor identifier
    if (memcmp(sSecBuffer[0] + 1, "CD001", 5)) {
      lg::warn("[OVERLORD ISO CD] Got the wrong volume descriptor identifier");
      char* cptr = (char*)sSecBuffer[0] + 1;
      printf("%c%c%c%c%c\n", cptr[0], cptr[1], cptr[2], cptr[3], cptr[4]);
      return 1;
    }

    // read path table into buffer
    uint32_t path_table_sector = ReadU32(sSecBuffer[0] + 0x8c);

    if (!ReadSectorsNow(path_table_sector, 1, sSecBuffer[0])) {
      lg::warn("[OVERLORD ISO CD] Failed to read path");
      return 1;
    }

    // read path table's extent into buffer
    uint32_t path_table_extent = ReadU32(sSecBuffer[0] + 2);

    if (!ReadSectorsNow(path_table_extent, 1, sSecBuffer[0])) {
      lg::warn("[OVERLORD ISO CD] Failed to read path table extent");
    }

    // read root directory
    add_files = true;
    uint32_t dir_size = ReadU32(sSecBuffer[0] + 10);
    if (!ReadDirectory(path_table_extent, dir_size, 0)) {
      lg::warn("[OVERLORD ISO CD] Failed to ReadDirectory");
      return 1;
    }

    // load filesystem stuff
    DecodeDUP(sSecBuffer[0]);
    LoadMusicTweaks(sSecBuffer[0]);
    LoadDiscID();

    // there's some sort of weird loop here over all file that does nothing.
    // my guess is its some commented out print thing?

    // empty load stack
    for (int i = 0; i < MAX_OPEN_FILES; i++) {
      sLoadStack[i].fr = nullptr;
    }

    // kill sector buffers
    for (int i = 0; i < 3; i++) {
      sSecBuffer[i] = nullptr;
    }
    return 0;
  } else {
    lg::warn("[OVERLORD ISO CD] Bad Media Type!");
    return 1;
  }
}

/*!
 * Find a file on the disc and return a FileRecord.
 * This is an ISO FS API Function
 */
FileRecord* FS_Find(const char* name) {
  char name_buff[16];
  MakeISOName(name_buff, name);
  return FS_FindIN(name_buff);
}

/*!
 * Find a file on the disc.  Uses the ISO name of the file.
 * This can be generated with MakeISOFile
 * This is an ISO FS API Function
 * There is a weird anti-piracy thing in here to prevent people from making copies with less than
 * 1 GB of data?  I guess you could remove the audio in languages you don't care about and put in
 * on a CD, and this would block this from happening.
 */
FileRecord* FS_FindIN(const char* iso_name) {
  const uint32_t* buff = (const uint32_t*)iso_name;
  for (;;) {            // this loop will spin forever if you have < 1 GB of files
    uint32_t size = 0;  // total sum of file sizes
    uint32_t count = 0;
    while (count < sNumFiles) {
      const uint32_t* ref = (uint32_t*)sFiles[count].name;
      if (ref[0] == buff[0] && ref[1] == buff[1] && ref[2] == buff[2]) {
        return sFiles + count;
      }
      size += sFiles[count].size;
      count++;
    }
    // if we get here, we haven't found the file, we should return 0 to indicate we don't have it
    // however, if we haven't found 1 GB of files after searching the whole thing
    // we assume that we've pirated the game and should continue looping
    // Note that the game attempts to load DUP files which will fails and will hit this condition.
    buff +=
        3;  // to make this look less suspicious, lets increment buff. also will crash eventually.
    if (0x3fffffff < size) {
      return nullptr;  // we got 1 GB of files, okay to return
    }

    // we didn't get 1 GB of files, you're a pirate.
    lg::warn("Pirated!");
  }
}

/*!
 * Determine the length of a file.
 * This is an ISO FS API Function
 */
uint32_t FS_GetLength(FileRecord* fr) {
  return fr->size;
}

/*!
 * Open a file by putting it on the load stack.
 * Set the offset to 0 or -1 if you do not want to have an offset.
 * This is an ISO FS API Function
 */
LoadStackEntry* FS_Open(FileRecord* fr, int32_t offset) {
  lg::info("[OVERLORD] FS Open {}", fr->name);
  LoadStackEntry* selected = nullptr;
  // find first unused spot on load stack.
  for (uint32_t i = 0; i < MAX_OPEN_FILES; i++) {
    if (!sLoadStack[i].fr) {
      selected = sLoadStack + i;
      selected->fr = fr;
      selected->location = fr->location;
      if (offset != -1) {
        selected->location += offset;
      }
      return selected;
    }
  }
  lg::warn("[OVERLORD ISO CD] Failed to FS_Open {}", fr->name);
  ExitIOP();
  return nullptr;
}

/*!
 * Open a file by putting it on the load stack.
 * Like Open, but allows an offset of -1 to be applied.
 * This is an ISO FS API Function
 */
LoadStackEntry* FS_OpenWad(FileRecord* fr, int32_t offset) {
  printf("[OVERLORD] FS Open %s\n", fr->name);
  LoadStackEntry* selected = nullptr;
  for (uint32_t i = 0; i < MAX_OPEN_FILES; i++) {
    if (!sLoadStack[i].fr) {
      selected = sLoadStack + i;
      selected->fr = fr;
      selected->location = fr->location + offset;
      return selected;
    }
  }
  lg::warn("[OVERLORD ISO CD] Failed to use FS_OpenWad {}", fr->name);
  ExitIOP();
  return nullptr;
}

/*!
 * Close an open file.
 * This is an ISO FS API Function
 */
void FS_Close(LoadStackEntry* fd) {
  lg::info("[OVERLORD] FS Close {}", fd->fr->name);
  if (fd == sReadInfo) {
    // the file is currently being read, so lets try to finish out the read, if possible.
    int count = 0;

    // the non-blocking sync, so we don't get stuck here on a catastrophic error.
    while (sceCdSync(1)) {
      DelayThread(1000);  // wait 1 ms and allow other stuff to run.
      count++;
      if (count == 1000) {  // waited too long to close this file
        sceCdBreak();       // interrupt the read
        break;
      }
    }
    sReadInfo = nullptr;
  }

  // close the FD
  fd->fr = nullptr;
}

/*!
 * Begin reading!  Returns FS_READ_OK on success (always)
 * This is an ISO FS API Function
 */
uint32_t FS_BeginRead(LoadStackEntry* fd, void* buffer, int32_t len) {
  // set the reading state:
  // I guess continuous stream buffer reads don't count as continuous?
  _continuous = (len == BUFFER_PAGE_SIZE) && (fd->location == (_sector + _sectors));
  _sector = fd->location;
  int32_t real_size = len;
  if (len < 0) {
    // not sure what this is about...
    lg::warn("[OVERLORD ISO CD] Negative length warning!");
    real_size = len + 0x7ff;
  }
  _sectors = real_size >> 11;
  _retries = 0;
  _buffer = buffer;
  GetSystemTime(&_starttime);

  // compute _real_sector
  SetRealSector();

  while (!sceCdRead(_real_sector, _sectors, _buffer, sMode)) {
    // error starting the read.  this is bad and possibly indicates somebody took the CD out.
    // lets wait for the CD to be ready again...
    CD_WaitReturn();
    _retries++;
    if (_sector >= sArea1) {
      // the original file we tried to read is duplicated...
      // so lets try reading the other copy of it!
      _dupseg = 1 - _dupseg;
      _continuous = 0;  // mark as noncontinuous read
      SetRealSector();  // recompute!
    }
  }

  // ??? this is strangely set up.
  if (len < 0) {
    len = len + 0x7ff;
  }

  fd->location += (len >> 0xb);

  // set sReadInfo to point to the current read.
  sReadInfo = fd;
  return CMD_STATUS_IN_PROGRESS;
}

/*!
 * Wait for current read to complete!
 * This is an ISO FS API Function
 * @return
 */
uint32_t FS_SyncRead() {
  // make sure a read is actually in progress
  if (!sReadInfo) {
    return CMD_STATUS_READ_ERR;
  }

  // remember when we start doing a SyncRead.
  SysClock now;
  GetSystemTime(&now);

  // block and wait for completion
  sceCdSync(0);
  // remember when we sync.
  GetSystemTime(&_endtime);

  // Loop to check if read succeed and start an additional read if not.
  while (sceCdGetError()) {
    // no, it didn't, lets retry
    _retries++;
    // toggle dupseg
    if (_sector >= sArea1) {
      _dupseg = 1 - _dupseg;
      _continuous = 0;
      SetRealSector();
    }

    // try until a read starts...
    while (!sceCdRead(_real_sector, _sectors, _buffer, sMode)) {
      // read start failed, possibly CD is removed
      CD_WaitReturn();
      // retry!
      _retries++;
      // toggle dupseg if possible
      if (_sector >= sArea1) {
        _dupseg = 1 - _dupseg;
        _continuous = 0;
        SetRealSector();
      }
    }

    // read has started
    // set dirty cd to indicate we had trouble
    gDirtyCd = 1;
    // wait for read to finish...
    sceCdSync(0);
    // if the read/sync fails, the loop will go again.
  }

  // Read complete! Mark CD as not dirty and clear active read!
  gDirtyCd = 0;
  sReadInfo = nullptr;

  // Optionally do some timing checks
  // (note that these never run because we don't have DUP files)
  // continuous read with no failures from dup zone
  // more than half the time spent in FS_SyncRead
  // Basically it tries to learn about which segment does worse in "too slow" reads
  // by averaging all historical too slow reads.  Once the other is winning by a certain amount
  // it will swap. It will also swap if there isn't enough samples on one.
  if (_retries == 0 && _continuous && _sectors >= sArea1 &&
      (_endtime.hi - _starttime.hi) / 2 < (_endtime.hi - now.hi)) {
    // record the time
    _times[_timesix++] = _endtime.hi - _starttime.hi;

    // if we filled the time buffer
    if (_timesix == TIME_SIZE) {
      // compute total time
      s32 total_time = 0;
      for (s32 i = 0; i < TIME_SIZE; i++) {
        total_time += _times[i];
      }

      // determine read speed
      gLastSpeed = 0x69780000 / (total_time >> 4);  // todo - work out this constant

      // add to average kps for this seg
      if (_tsamps[_dupseg] < 0x40000) {
        _tkps[_dupseg] = _tkps[_dupseg] + gLastSpeed;
        _tsamps[_dupseg] = _tsamps[_dupseg];
      }

      // average speed of this segment
      gDiskSpeed[_dupseg] = _tkps[_dupseg] / _tsamps[_dupseg];

      _timesix = 0;

      if (_tsamps[0] < 8) {
        // not much information about segment 0
        if (_dupseg) {
          // and we aren't reading segment 0...
          // so let's read segment 0
          _dupseg = 0;
          _sector = 0;
        }
      } else {
        // got enough info about segment 0.
        if (_tsamps[1] < 8) {
          // not enough information about segment 1
          if (!_dupseg) {
            // and not reading, so lets read it.
            _dupseg = 1;
            _sector = 0;
          }
        } else {
          // enough info about both.
          if ((_tkps[1] / _tsamps[1] + 0x32) < (_tkps[0] / _tsamps[0])) {
            // section 0 wins by at least 0x32, lets use it if we aren't already
            if (_dupseg) {
              _dupseg = 0;
              _sector = 0;
            }
          } else if ((_tkps[0] / _tsamps[0] + 0x32) < (_tkps[1] / _tsamps[1])) {
            if (!_dupseg) {
              _dupseg = 1;
              _sector = 0;
            }
          }
        }
      }
      // set our current decision in a global for the EE to read.
      gDupSeg = _dupseg;
    }
  }
  return CMD_STATUS_IN_PROGRESS;
}

/*!
 * Load a SoundBank now. Doesn't do any fancy read stuff.
 */
uint32_t FS_LoadSoundBank(char* name, void* buffer) {
  char full_name[32];  // may actually be 8, but lets be safe

  // ??? todo this is probably a field of the buffer.
  u32 header_size;
  if (*(s32*)(((u8*)buffer) + 0x14) == 0x65) {
    header_size = 1;
  } else {
    header_size = 10;
  }

  if (strlen(name) > 16) {
    printf("[OVERLORD ISO CD] FS_LoadSoundBank has an invalid name!\n");
  }

  // append .sbk
  strcpy(full_name, name);
  strcat(full_name, ".sbk");

  FileRecord* fr = FS_Find(full_name);
  if (!fr) {
    printf("[OVERLORD ISO CD] FS_LoadSoundBank cannot find bank %s, loading empty instead.\n",
           full_name);
    fr = FS_Find("empty1.sbk");
  }

  // hack to do a read now (the Sound Bank loads bypass all the other fancy loading stuff evidently)
  _sector = fr->location;
  SetRealSector();

  // loop until we read header successfully.
  // don't set retries or dirty cd
  while (!ReadSectorsNow(_real_sector, header_size, buffer)) {
    // ReadSectorsNow will only return if the read fails to start.  in this case we assume the disc
    // was removed:
    CD_WaitReturn();
    // we don't increment retries...
    if (_sector >= sArea1) {
      _dupseg = 1 - _dupseg;
      _continuous = 0;
      SetRealSector();
    }
  }

  // now have the sound library do a load.
  // (this time we set dirty cd if it fails, but no retries)
  auto load_status = snd_BankLoadByLoc(_real_sector + header_size, 0);
  while (!load_status && snd_GetLastLoadError() < 0x100) {
    CD_WaitReturn();
    if (_sector >= sArea1) {
      _dupseg = 1 - _dupseg;
      _continuous = 0;
      SetRealSector();
    }
    load_status = snd_BankLoadByLoc(_real_sector + header_size, 0);
    if (!load_status) {
      gDirtyCd = 1;
    }
  }
  gDirtyCd = 0;

  // pirate check sometimes
  sound_bank_loads++;
  if ((sound_bank_loads & 7) == 0) {
    pirated = 1;
    // check that one file is past sector 0x80000 (approx 1 GB)
    for (u32 i = 0; i < sNumFiles; i++) {
      if (sFiles[i].location + (sFiles[i].size >> 11) > 0x80000) {
        pirated = 0;
      }
    }
  }

  snd_ResolveBankXREFS();
  PrintBankInfo((SoundBank*)buffer);
  _sector = 0;
  // ??? todo this is probably a field of the buffer.
  *(s32*)(((u8*)buffer) + 0x10) = load_status;
  return 0;
}

/*!
 * Load a music file. Load now, doesn't do fancy reading stuff.
 */
uint32_t FS_LoadMusic(char* name, void* buffer) {
  char full_name[32];  // may actually be 8, but lets be safe
  if (strlen(name) > 16) {
    printf("[OVERLORD ISO CD] FS_LoadMusic has an invalid name!\n");
  }

  // append .mus
  strcpy(full_name, name);
  strcat(full_name, ".mus");

  FileRecord* fr = FS_Find(full_name);
  if (!fr) {
    printf("[OVERLORD ISO CD] FS_LoadMusic cannot find bank %s.\n", full_name);
    return 6;
  }

  _sector = fr->location;
  SetRealSector();
  // another "piracy" check to make sure the media is the correct type...
  (*cdmmode)(SCECdDVD);

  // now have the sound library do a load.
  auto load_status = snd_BankLoadByLoc(_real_sector, 0);
  // TODO magic constant 0x100
  while (!load_status && snd_GetLastLoadError() < 0x100) {
    CD_WaitReturn();
    if (_sector >= sArea1) {
      _dupseg = 1 - _dupseg;
      _continuous = 0;
      SetRealSector();
    }
    load_status = snd_BankLoadByLoc(_real_sector, 0);
    if (!load_status) {
      gDirtyCd = 1;
    }
  }
  gDirtyCd = 0;
  snd_ResolveBankXREFS();
  _sector = 0;
  *(s32*)buffer = load_status;
  return 0;
}

/*!
 * Make sure the drive is happy.
 * NOTE - only call this when the drive should have nothing to do!
 */
void FS_PollDrive() {
  if (sceCdDiskReady(1) == SCECdNotReady) {  // non-blocking
    CD_WaitReturn();
  }
}

/*!
 * Wait for the game CD/DVD to be put back in the playstation.
 * Only call this if you think the CD/DVD has been removed, as requires a seek.
 */
void CD_WaitReturn() {
  gNoCD = 1;
  do {
    while (sceCdDiskReady(1) == SCECdNotReady) {
    }
  } while (!CheckDiskID());
  gNoCD = 0;
}
