// Copyright 2008 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.

#include <cinttypes>
#include <cmath>
#include <memory>
#include <string>

#include "AudioCommon/AudioCommon.h"

#include "Common/ChunkFile.h"
#include "Common/CommonTypes.h"
#include "Common/MathUtil.h"

#include "Core/ConfigManager.h"
#include "Core/Core.h"
#include "Core/CoreTiming.h"
#include "Core/HW/AudioInterface.h"
#include "Core/HW/DVDInterface.h"
#include "Core/HW/DVDThread.h"
#include "Core/HW/MMIO.h"
#include "Core/HW/Memmap.h"
#include "Core/HW/ProcessorInterface.h"
#include "Core/HW/StreamADPCM.h"
#include "Core/HW/SystemTimers.h"
#include "Core/IPC_HLE/WII_IPC_HLE.h"
#include "Core/IPC_HLE/WII_IPC_HLE_Device_DI.h"
#include "Core/Movie.h"

#include "DiscIO/Volume.h"
#include "DiscIO/VolumeCreator.h"

static const double PI = 3.14159265358979323846264338328;

// Rate the drive can transfer data to main memory, given the data
// is already buffered. Measured in bytes per second.
static const u32 BUFFER_TRANSFER_RATE = 1024 * 1024 * 16;

// Disc access time measured in milliseconds
static const u32 DISC_ACCESS_TIME_MS = 50;

// The size of a Wii disc layer in bytes (is this correct?)
static const u64 WII_DISC_LAYER_SIZE = 4699979776;

// By knowing the disc read speed at two locations defined here,
// the program can calulate the speed at arbitrary locations.
// Offsets are in bytes, and speeds are in bytes per second.
//
// These speeds are approximations of speeds measured on real Wiis.

static const u32 GC_DISC_LOCATION_1_OFFSET = 0;  // The beginning of a GC disc - 48 mm
static const u32 GC_DISC_LOCATION_1_READ_SPEED = (u32)(1024 * 1024 * 2.1);
static const u32 GC_DISC_LOCATION_2_OFFSET = 1459978239;  // The end of a GC disc - 76 mm
static const u32 GC_DISC_LOCATION_2_READ_SPEED = (u32)(1024 * 1024 * 3.325);

static const u32 WII_DISC_LOCATION_1_OFFSET = 0;  // The beginning of a Wii disc - 48 mm
static const u32 WII_DISC_LOCATION_1_READ_SPEED = (u32)(1024 * 1024 * 3.5);
static const u64 WII_DISC_LOCATION_2_OFFSET =
    WII_DISC_LAYER_SIZE;  // The end of a Wii disc - 116 mm
static const u32 WII_DISC_LOCATION_2_READ_SPEED = (u32)(1024 * 1024 * 8.45);

// These values are used for disc read speed calculations. Calculations
// are done using an arbitrary length unit where the radius of a disc track
// is the same as the read speed at that track in bytes per second.

static const double GC_DISC_AREA_UP_TO_LOCATION_1 =
    PI * GC_DISC_LOCATION_1_READ_SPEED * GC_DISC_LOCATION_1_READ_SPEED;
static const double GC_DISC_AREA_UP_TO_LOCATION_2 =
    PI * GC_DISC_LOCATION_2_READ_SPEED * GC_DISC_LOCATION_2_READ_SPEED;
static const double GC_BYTES_PER_AREA_UNIT =
    (GC_DISC_LOCATION_2_OFFSET - GC_DISC_LOCATION_1_OFFSET) /
    (GC_DISC_AREA_UP_TO_LOCATION_2 - GC_DISC_AREA_UP_TO_LOCATION_1);

static const double WII_DISC_AREA_UP_TO_LOCATION_1 =
    PI * WII_DISC_LOCATION_1_READ_SPEED * WII_DISC_LOCATION_1_READ_SPEED;
static const double WII_DISC_AREA_UP_TO_LOCATION_2 =
    PI * WII_DISC_LOCATION_2_READ_SPEED * WII_DISC_LOCATION_2_READ_SPEED;
static const double WII_BYTES_PER_AREA_UNIT =
    (WII_DISC_LOCATION_2_OFFSET - WII_DISC_LOCATION_1_OFFSET) /
    (WII_DISC_AREA_UP_TO_LOCATION_2 - WII_DISC_AREA_UP_TO_LOCATION_1);

namespace DVDInterface
{
// internal hardware addresses
enum
{
  DI_STATUS_REGISTER = 0x00,
  DI_COVER_REGISTER = 0x04,
  DI_COMMAND_0 = 0x08,
  DI_COMMAND_1 = 0x0C,
  DI_COMMAND_2 = 0x10,
  DI_DMA_ADDRESS_REGISTER = 0x14,
  DI_DMA_LENGTH_REGISTER = 0x18,
  DI_DMA_CONTROL_REGISTER = 0x1C,
  DI_IMMEDIATE_DATA_BUFFER = 0x20,
  DI_CONFIG_REGISTER = 0x24
};

// debug commands which may be ORd
enum
{
  STOP_DRIVE = 0,
  START_DRIVE = 0x100,
  ACCEPT_COPY = 0x4000,
  DISC_CHECK = 0x8000,
};

// DI Status Register
union UDISR {
  u32 Hex;
  struct
  {
    u32 BREAK : 1;       // Stop the Device + Interrupt
    u32 DEINITMASK : 1;  // Access Device Error Int Mask
    u32 DEINT : 1;       // Access Device Error Int
    u32 TCINTMASK : 1;   // Transfer Complete Int Mask
    u32 TCINT : 1;       // Transfer Complete Int
    u32 BRKINTMASK : 1;
    u32 BRKINT : 1;  // w 1: clear brkint
    u32 : 25;
  };
  UDISR() { Hex = 0; }
  UDISR(u32 _hex) { Hex = _hex; }
};

// DI Cover Register
union UDICVR {
  u32 Hex;
  struct
  {
    u32 CVR : 1;         // 0: Cover closed  1: Cover open
    u32 CVRINTMASK : 1;  // 1: Interrupt enabled
    u32 CVRINT : 1;      // r 1: Interrupt requested w 1: Interrupt clear
    u32 : 29;
  };
  UDICVR() { Hex = 0; }
  UDICVR(u32 _hex) { Hex = _hex; }
};

union UDICMDBUF {
  u32 Hex;
  struct
  {
    u8 CMDBYTE3;
    u8 CMDBYTE2;
    u8 CMDBYTE1;
    u8 CMDBYTE0;
  };
};

// DI DMA Address Register
union UDIMAR {
  u32 Hex;
  struct
  {
    u32 Zerobits : 5;  // Must be zero (32byte aligned)
    u32 : 27;
  };
  struct
  {
    u32 Address : 26;
    u32 : 6;
  };
};

// DI DMA Address Length Register
union UDILENGTH {
  u32 Hex;
  struct
  {
    u32 Zerobits : 5;  // Must be zero (32byte aligned)
    u32 : 27;
  };
  struct
  {
    u32 Length : 26;
    u32 : 6;
  };
};

// DI DMA Control Register
union UDICR {
  u32 Hex;
  struct
  {
    u32 TSTART : 1;  // w:1 start   r:0 ready
    u32 DMA : 1;     // 1: DMA Mode    0: Immediate Mode (can only do Access Register Command)
    u32 RW : 1;      // 0: Read Command (DVD to Memory)  1: Write Command (Memory to DVD)
    u32 : 29;
  };
};

union UDIIMMBUF {
  u32 Hex;
  struct
  {
    u8 REGVAL3;
    u8 REGVAL2;
    u8 REGVAL1;
    u8 REGVAL0;
  };
};

// DI Config Register
union UDICFG {
  u32 Hex;
  struct
  {
    u32 CONFIG : 8;
    u32 : 24;
  };
  UDICFG() { Hex = 0; }
  UDICFG(u32 _hex) { Hex = _hex; }
};

static std::unique_ptr<DiscIO::IVolume> s_inserted_volume;

// STATE_TO_SAVE
// hardware registers
static UDISR s_DISR;
static UDICVR s_DICVR;
static UDICMDBUF s_DICMDBUF[3];
static UDIMAR s_DIMAR;
static UDILENGTH s_DILENGTH;
static UDICR s_DICR;
static UDIIMMBUF s_DIIMMBUF;
static UDICFG s_DICFG;

static u32 s_audio_position;
static u32 s_current_start;
static u32 s_current_length;
static u32 s_next_start;
static u32 s_next_length;

static u32 s_error_code = 0;
static bool s_disc_inside = false;
static bool s_stream = false;
static bool s_stop_at_track_end = false;
static int s_finish_executing_command = 0;
static int s_dtk = 0;

static u64 s_last_read_offset;
static u64 s_last_read_time;

// GC-AM only
static unsigned char s_media_buffer[0x40];

static int s_eject_disc;
static int s_insert_disc;

static void EjectDiscCallback(u64 userdata, s64 cyclesLate);
static void InsertDiscCallback(u64 userdata, s64 cyclesLate);
static void FinishExecutingCommandCallback(u64 userdata, s64 cycles_late);

void SetLidOpen(bool _bOpen);

void UpdateInterrupts();
void GenerateDIInterrupt(DIInterruptType _DVDInterrupt);

void WriteImmediate(u32 value, u32 output_address, bool reply_to_ios);
bool ExecuteReadCommand(u64 DVD_offset, u32 output_address, u32 DVD_length, u32 output_length,
                        bool decrypt, bool reply_to_ios, DIInterruptType* interrupt_type,
                        u64* ticks_until_completion);

u64 SimulateDiscReadTime(u64 offset, u32 length);
s64 CalculateRawDiscReadTime(u64 offset, s64 length);

void DoState(PointerWrap& p)
{
  p.DoPOD(s_DISR);
  p.DoPOD(s_DICVR);
  p.DoArray(s_DICMDBUF);
  p.Do(s_DIMAR);
  p.Do(s_DILENGTH);
  p.Do(s_DICR);
  p.Do(s_DIIMMBUF);
  p.DoPOD(s_DICFG);

  p.Do(s_next_start);
  p.Do(s_audio_position);
  p.Do(s_next_length);

  p.Do(s_error_code);
  p.Do(s_disc_inside);
  p.Do(s_stream);

  p.Do(s_current_start);
  p.Do(s_current_length);

  p.Do(s_last_read_offset);
  p.Do(s_last_read_time);

  p.Do(s_stop_at_track_end);

  DVDThread::DoState(p);
}

static u32 ProcessDTKSamples(short* tempPCM, u32 num_samples)
{
  // TODO: Read audio data using the DVD thread instead of blocking on it?
  DVDThread::WaitUntilIdle();

  u32 samples_processed = 0;
  do
  {
    if (s_audio_position >= s_current_start + s_current_length)
    {
      DEBUG_LOG(DVDINTERFACE, "ProcessDTKSamples: "
                              "NextStart=%08x,NextLength=%08x,CurrentStart=%08x,CurrentLength=%08x,"
                              "AudioPos=%08x",
                s_next_start, s_next_length, s_current_start, s_current_length, s_audio_position);

      s_audio_position = s_next_start;
      s_current_start = s_next_start;
      s_current_length = s_next_length;

      if (s_stop_at_track_end)
      {
        s_stop_at_track_end = false;
        s_stream = false;
        break;
      }

      StreamADPCM::InitFilter();
    }

    u8 tempADPCM[StreamADPCM::ONE_BLOCK_SIZE];
    // TODO: What if we can't read from s_audio_position?
    s_inserted_volume->Read(s_audio_position, sizeof(tempADPCM), tempADPCM, false);
    s_audio_position += sizeof(tempADPCM);
    StreamADPCM::DecodeBlock(tempPCM + samples_processed * 2, tempADPCM);
    samples_processed += StreamADPCM::SAMPLES_PER_BLOCK;
  } while (samples_processed < num_samples);
  for (unsigned i = 0; i < samples_processed * 2; ++i)
  {
    // TODO: Fix the mixer so it can accept non-byte-swapped samples.
    tempPCM[i] = Common::swap16(tempPCM[i]);
  }
  return samples_processed;
}

static void DTKStreamingCallback(u64 userdata, s64 cyclesLate)
{
  // Send audio to the mixer.
  static const int NUM_SAMPLES = 48000 / 2000 * 7;  // 3.5ms of 48kHz samples
  short tempPCM[NUM_SAMPLES * 2];
  unsigned samples_processed;
  if (s_stream && AudioInterface::IsPlaying())
  {
    samples_processed = ProcessDTKSamples(tempPCM, NUM_SAMPLES);
  }
  else
  {
    memset(tempPCM, 0, sizeof(tempPCM));
    samples_processed = NUM_SAMPLES;
  }
  g_sound_stream->GetMixer()->PushStreamingSamples(tempPCM, samples_processed);

  int ticks_to_dtk = int(SystemTimers::GetTicksPerSecond() * u64(samples_processed) / 48000);
  CoreTiming::ScheduleEvent(ticks_to_dtk - cyclesLate, s_dtk);
}

void Init()
{
  DVDThread::Start();

  s_DISR.Hex = 0;
  s_DICVR.Hex = 1;  // Disc Channel relies on cover being open when no disc is inserted
  s_DICMDBUF[0].Hex = 0;
  s_DICMDBUF[1].Hex = 0;
  s_DICMDBUF[2].Hex = 0;
  s_DIMAR.Hex = 0;
  s_DILENGTH.Hex = 0;
  s_DICR.Hex = 0;
  s_DIIMMBUF.Hex = 0;
  s_DICFG.Hex = 0;
  s_DICFG.CONFIG = 1;  // Disable bootrom descrambler

  s_audio_position = 0;
  s_next_start = 0;
  s_next_length = 0;
  s_current_start = 0;
  s_current_length = 0;

  s_error_code = 0;
  s_disc_inside = false;
  s_stream = false;
  s_stop_at_track_end = false;

  s_last_read_offset = 0;
  s_last_read_time = 0;

  s_eject_disc = CoreTiming::RegisterEvent("EjectDisc", EjectDiscCallback);
  s_insert_disc = CoreTiming::RegisterEvent("InsertDisc", InsertDiscCallback);

  s_finish_executing_command =
      CoreTiming::RegisterEvent("FinishExecutingCommand", FinishExecutingCommandCallback);
  s_dtk = CoreTiming::RegisterEvent("StreamingTimer", DTKStreamingCallback);

  CoreTiming::ScheduleEvent(0, s_dtk);
}

void Shutdown()
{
  DVDThread::Stop();
  s_inserted_volume.reset();
}

const DiscIO::IVolume& GetVolume()
{
  return *s_inserted_volume;
}

bool SetVolumeName(const std::string& disc_path)
{
  DVDThread::WaitUntilIdle();
  s_inserted_volume = DiscIO::CreateVolumeFromFilename(disc_path);
  return VolumeIsValid();
}

bool SetVolumeDirectory(const std::string& full_path, bool is_wii,
                        const std::string& apploader_path, const std::string& DOL_path)
{
  DVDThread::WaitUntilIdle();
  s_inserted_volume =
      DiscIO::CreateVolumeFromDirectory(full_path, is_wii, apploader_path, DOL_path);
  return VolumeIsValid();
}

bool VolumeIsValid()
{
  return s_inserted_volume != nullptr;
}

void SetDiscInside(bool disc_inside)
{
  if (s_disc_inside != disc_inside)
    SetLidOpen(!disc_inside);

  s_disc_inside = disc_inside;
}

bool IsDiscInside()
{
  return s_disc_inside;
}

// Take care of all logic of "swapping discs"
// We want this in the "backend", NOT the gui
// any !empty string will be deleted to ensure
// that the userdata string exists when called
static void EjectDiscCallback(u64 userdata, s64 cyclesLate)
{
  DVDThread::WaitUntilIdle();
  s_inserted_volume.reset();
  SetDiscInside(false);
}

static void InsertDiscCallback(u64 userdata, s64 cyclesLate)
{
  std::string& SavedFileName = SConfig::GetInstance().m_strFilename;
  std::string* _FileName = (std::string*)userdata;

  if (!SetVolumeName(*_FileName))
  {
    // Put back the old one
    SetVolumeName(SavedFileName);
    PanicAlertT("Invalid file");
  }
  SetDiscInside(VolumeIsValid());
  delete _FileName;
}

void ChangeDisc(const std::string& newFileName)
{
  // WARNING: Can only run on Host Thread
  bool was_unpaused = Core::PauseAndLock(true);
  std::string* _FileName = new std::string(newFileName);
  CoreTiming::ScheduleEvent(0, s_eject_disc);
  CoreTiming::ScheduleEvent(500000000, s_insert_disc, (u64)_FileName);
  if (Movie::IsRecordingInput())
  {
    Movie::g_bDiscChange = true;
    std::string fileName = newFileName;
    auto sizeofpath = fileName.find_last_of("/\\") + 1;
    if (fileName.substr(sizeofpath).length() > 40)
    {
      PanicAlertT("The disc change to \"%s\" could not be saved in the .dtm file.\n"
                  "The filename of the disc image must not be longer than 40 characters.",
                  newFileName.c_str());
    }
    Movie::g_discChange = fileName.substr(sizeofpath);
  }
  Core::PauseAndLock(false, was_unpaused);
}

void SetLidOpen(bool open)
{
  s_DICVR.CVR = open ? 1 : 0;

  GenerateDIInterrupt(INT_CVRINT);
}

bool ChangePartition(u64 offset)
{
  DVDThread::WaitUntilIdle();
  return s_inserted_volume->ChangePartition(offset);
}

void RegisterMMIO(MMIO::Mapping* mmio, u32 base)
{
  mmio->Register(base | DI_STATUS_REGISTER, MMIO::DirectRead<u32>(&s_DISR.Hex),
                 MMIO::ComplexWrite<u32>([](u32, u32 val) {
                   UDISR tmpStatusReg(val);

                   s_DISR.DEINITMASK = tmpStatusReg.DEINITMASK;
                   s_DISR.TCINTMASK = tmpStatusReg.TCINTMASK;
                   s_DISR.BRKINTMASK = tmpStatusReg.BRKINTMASK;
                   s_DISR.BREAK = tmpStatusReg.BREAK;

                   if (tmpStatusReg.DEINT)
                     s_DISR.DEINT = 0;

                   if (tmpStatusReg.TCINT)
                     s_DISR.TCINT = 0;

                   if (tmpStatusReg.BRKINT)
                     s_DISR.BRKINT = 0;

                   if (s_DISR.BREAK)
                   {
                     _dbg_assert_(DVDINTERFACE, 0);
                   }

                   UpdateInterrupts();
                 }));

  mmio->Register(base | DI_COVER_REGISTER, MMIO::DirectRead<u32>(&s_DICVR.Hex),
                 MMIO::ComplexWrite<u32>([](u32, u32 val) {
                   UDICVR tmpCoverReg(val);

                   s_DICVR.CVRINTMASK = tmpCoverReg.CVRINTMASK;

                   if (tmpCoverReg.CVRINT)
                     s_DICVR.CVRINT = 0;

                   UpdateInterrupts();
                 }));

  // Command registers are very similar and we can register them with a
  // simple loop.
  for (int i = 0; i < 3; ++i)
    mmio->Register(base | (DI_COMMAND_0 + 4 * i), MMIO::DirectRead<u32>(&s_DICMDBUF[i].Hex),
                   MMIO::DirectWrite<u32>(&s_DICMDBUF[i].Hex));

  // DMA related registers. Mostly direct accesses (+ masking for writes to
  // handle things like address alignment) and complex write on the DMA
  // control register that will trigger the DMA.
  mmio->Register(base | DI_DMA_ADDRESS_REGISTER, MMIO::DirectRead<u32>(&s_DIMAR.Hex),
                 MMIO::DirectWrite<u32>(&s_DIMAR.Hex, ~0xFC00001F));
  mmio->Register(base | DI_DMA_LENGTH_REGISTER, MMIO::DirectRead<u32>(&s_DILENGTH.Hex),
                 MMIO::DirectWrite<u32>(&s_DILENGTH.Hex, ~0x1F));
  mmio->Register(base | DI_DMA_CONTROL_REGISTER, MMIO::DirectRead<u32>(&s_DICR.Hex),
                 MMIO::ComplexWrite<u32>([](u32, u32 val) {
                   s_DICR.Hex = val & 7;
                   if (s_DICR.TSTART)
                   {
                     ExecuteCommand(s_DICMDBUF[0].Hex, s_DICMDBUF[1].Hex, s_DICMDBUF[2].Hex,
                                    s_DIMAR.Hex, s_DILENGTH.Hex, false);
                   }
                 }));

  mmio->Register(base | DI_IMMEDIATE_DATA_BUFFER, MMIO::DirectRead<u32>(&s_DIIMMBUF.Hex),
                 MMIO::DirectWrite<u32>(&s_DIIMMBUF.Hex));

  // DI config register is read only.
  mmio->Register(base | DI_CONFIG_REGISTER, MMIO::DirectRead<u32>(&s_DICFG.Hex),
                 MMIO::InvalidWrite<u32>());
}

void UpdateInterrupts()
{
  if ((s_DISR.DEINT & s_DISR.DEINITMASK) || (s_DISR.TCINT & s_DISR.TCINTMASK) ||
      (s_DISR.BRKINT & s_DISR.BRKINTMASK) || (s_DICVR.CVRINT & s_DICVR.CVRINTMASK))
  {
    ProcessorInterface::SetInterrupt(ProcessorInterface::INT_CAUSE_DI, true);
  }
  else
  {
    ProcessorInterface::SetInterrupt(ProcessorInterface::INT_CAUSE_DI, false);
  }

  // Required for Summoner: A Goddess Reborn
  CoreTiming::ForceExceptionCheck(50);
}

void GenerateDIInterrupt(DIInterruptType dvd_interrupt)
{
  switch (dvd_interrupt)
  {
  case INT_DEINT:
    s_DISR.DEINT = 1;
    break;
  case INT_TCINT:
    s_DISR.TCINT = 1;
    break;
  case INT_BRKINT:
    s_DISR.BRKINT = 1;
    break;
  case INT_CVRINT:
    s_DICVR.CVRINT = 1;
    break;
  }

  UpdateInterrupts();
}

void WriteImmediate(u32 value, u32 output_address, bool reply_to_ios)
{
  if (reply_to_ios)
    Memory::Write_U32(value, output_address);
  else
    s_DIIMMBUF.Hex = value;
}

// Iff false is returned, ScheduleEvent must be used to finish executing the command
bool ExecuteReadCommand(u64 DVD_offset, u32 output_address, u32 DVD_length, u32 output_length,
                        bool decrypt, bool reply_to_ios, DIInterruptType* interrupt_type,
                        u64* ticks_until_completion)
{
  if (!s_disc_inside)
  {
    // Disc read fails
    s_error_code = ERROR_NO_DISK | ERROR_COVER_H;
    *interrupt_type = INT_DEINT;
    return false;
  }
  else
  {
    // Disc read succeeds
    *interrupt_type = INT_TCINT;
  }

  if (DVD_length > output_length)
  {
    WARN_LOG(
        DVDINTERFACE,
        "Detected attempt to read more data from the DVD than fit inside the out buffer. Clamp.");
    DVD_length = output_length;
  }

  if (SConfig::GetInstance().bFastDiscSpeed)
    // An optional hack to speed up loading times
    *ticks_until_completion =
        output_length * (SystemTimers::GetTicksPerSecond() / BUFFER_TRANSFER_RATE);
  else
    *ticks_until_completion = SimulateDiscReadTime(DVD_offset, DVD_length);

  DVDThread::StartRead(DVD_offset, output_address, DVD_length, decrypt, reply_to_ios,
                       (int)*ticks_until_completion);
  return true;
}

// When the command has finished executing, callback_event_type
// will be called using CoreTiming::ScheduleEvent,
// with the userdata set to the interrupt type.
void ExecuteCommand(u32 command_0, u32 command_1, u32 command_2, u32 output_address,
                    u32 output_length, bool reply_to_ios)
{
  DIInterruptType interrupt_type = INT_TCINT;
  u64 ticks_until_completion = SystemTimers::GetTicksPerSecond() / 15000;
  bool command_handled_by_thread = false;

  bool GCAM = (SConfig::GetInstance().m_SIDevice[0] == SIDEVICE_AM_BASEBOARD) &&
              (SConfig::GetInstance().m_EXIDevice[2] == EXIDEVICE_AM_BASEBOARD);

  // DVDLowRequestError needs access to the error code set by the previous command
  if (command_0 >> 24 != DVDLowRequestError)
    s_error_code = 0;

  if (GCAM)
  {
    ERROR_LOG(DVDINTERFACE, "DVD: %08x, %08x, %08x, DMA=addr:%08x,len:%08x,ctrl:%08x", command_0,
              command_1, command_2, output_address, output_length, s_DICR.Hex);
    // decrypt command. But we have a zero key, that simplifies things a lot.
    // If you get crazy dvd command errors, make sure 0x80000000 - 0x8000000c is zero'd
    command_0 <<= 24;
  }

  switch (command_0 >> 24)
  {
  // Seems to be used by both GC and Wii
  case DVDLowInquiry:
    if (GCAM)
    {
      // 0x29484100...
      // was 21 i'm not entirely sure about this, but it works well.
      WriteImmediate(0x21000000, output_address, reply_to_ios);
    }
    else
    {
      // (shuffle2) Taken from my Wii
      Memory::Write_U32(0x00000002, output_address);
      Memory::Write_U32(0x20060526, output_address + 4);
      // This was in the oubuf even though this cmd is only supposed to reply with 64bits
      // However, this and other tests strongly suggest that the buffer is static, and it's never -
      // or rarely cleared.
      Memory::Write_U32(0x41000000, output_address + 8);

      INFO_LOG(DVDINTERFACE, "DVDLowInquiry (Buffer 0x%08x, 0x%x)", output_address, output_length);
    }
    break;

  // Only seems to be used from WII_IPC, not through direct access
  case DVDLowReadDiskID:
    INFO_LOG(DVDINTERFACE, "DVDLowReadDiskID");
    command_handled_by_thread =
        ExecuteReadCommand(0, output_address, 0x20, output_length, false, reply_to_ios,
                           &interrupt_type, &ticks_until_completion);
    break;

  // Only used from WII_IPC. This is the only read command that decrypts data
  case DVDLowRead:
    INFO_LOG(DVDINTERFACE, "DVDLowRead: DVDAddr: 0x%09" PRIx64 ", Size: 0x%x", (u64)command_2 << 2,
             command_1);
    command_handled_by_thread =
        ExecuteReadCommand((u64)command_2 << 2, output_address, command_1, output_length, true,
                           reply_to_ios, &interrupt_type, &ticks_until_completion);
    break;

  // Probably only used by Wii
  case DVDLowWaitForCoverClose:
    INFO_LOG(DVDINTERFACE, "DVDLowWaitForCoverClose");
    interrupt_type = (DIInterruptType)4;  // ???
    break;

  // "Set Extension"...not sure what it does. GC only?
  case 0x55:
    INFO_LOG(DVDINTERFACE, "SetExtension");
    break;

  // Probably only used though WII_IPC
  case DVDLowGetCoverReg:
    WriteImmediate(s_DICVR.Hex, output_address, reply_to_ios);
    INFO_LOG(DVDINTERFACE, "DVDLowGetCoverReg 0x%08x", s_DICVR.Hex);
    break;

  // Probably only used by Wii
  case DVDLowNotifyReset:
    ERROR_LOG(DVDINTERFACE, "DVDLowNotifyReset");
    PanicAlert("DVDLowNotifyReset");
    break;
  // Probably only used by Wii
  case DVDLowReadDvdPhysical:
    ERROR_LOG(DVDINTERFACE, "DVDLowReadDvdPhysical");
    PanicAlert("DVDLowReadDvdPhysical");
    break;
  // Probably only used by Wii
  case DVDLowReadDvdCopyright:
    ERROR_LOG(DVDINTERFACE, "DVDLowReadDvdCopyright");
    PanicAlert("DVDLowReadDvdCopyright");
    break;
  // Probably only used by Wii
  case DVDLowReadDvdDiscKey:
    ERROR_LOG(DVDINTERFACE, "DVDLowReadDvdDiscKey");
    PanicAlert("DVDLowReadDvdDiscKey");
    break;

  // Probably only used by Wii
  case DVDLowClearCoverInterrupt:
    INFO_LOG(DVDINTERFACE, "DVDLowClearCoverInterrupt");
    s_DICVR.CVRINT = 0;
    break;

  // Probably only used by Wii
  case DVDLowGetCoverStatus:
    WriteImmediate(s_disc_inside ? 2 : 1, output_address, reply_to_ios);
    INFO_LOG(DVDINTERFACE, "DVDLowGetCoverStatus: Disc %sInserted", s_disc_inside ? "" : "Not ");
    break;

  // Probably only used by Wii
  case DVDLowReset:
    INFO_LOG(DVDINTERFACE, "DVDLowReset");
    break;

  // Probably only used by Wii
  case DVDLowClosePartition:
    INFO_LOG(DVDINTERFACE, "DVDLowClosePartition");
    break;

  // Probably only used by Wii
  case DVDLowUnencryptedRead:
    INFO_LOG(DVDINTERFACE, "DVDLowUnencryptedRead: DVDAddr: 0x%09" PRIx64 ", Size: 0x%x",
             (u64)command_2 << 2, command_1);

    // We must make sure it is in a valid area! (#001 check)
    // Are these checks correct? They seem to mix 32-bit offsets and 8-bit lengths
    // * 0x00000000 - 0x00014000 (limit of older IOS versions)
    // * 0x460a0000 - 0x460a0008
    // * 0x7ed40000 - 0x7ed40008
    if (((command_2 > 0x00000000 && command_2 < 0x00014000) ||
         (((command_2 + command_1) > 0x00000000) && (command_2 + command_1) < 0x00014000) ||
         (command_2 > 0x460a0000 && command_2 < 0x460a0008) ||
         (((command_2 + command_1) > 0x460a0000) && (command_2 + command_1) < 0x460a0008) ||
         (command_2 > 0x7ed40000 && command_2 < 0x7ed40008) ||
         (((command_2 + command_1) > 0x7ed40000) && (command_2 + command_1) < 0x7ed40008)))
    {
      command_handled_by_thread =
          ExecuteReadCommand((u64)command_2 << 2, output_address, command_1, output_length, false,
                             reply_to_ios, &interrupt_type, &ticks_until_completion);
    }
    else
    {
      WARN_LOG(DVDINTERFACE, "DVDLowUnencryptedRead: trying to read out of bounds @ %09" PRIx64,
               (u64)command_2 << 2);
      s_error_code = ERROR_READY | ERROR_BLOCK_OOB;
      // Should cause software to call DVDLowRequestError
      interrupt_type = INT_BRKINT;
    }

    break;

  // Probably only used by Wii
  case DVDLowEnableDvdVideo:
    ERROR_LOG(DVDINTERFACE, "DVDLowEnableDvdVideo");
    break;

  // New Super Mario Bros. Wii sends these commands,
  // but it seems we don't need to implement anything.
  // Probably only used by Wii
  case 0x95:
  case 0x96:
    ERROR_LOG(DVDINTERFACE, "Unimplemented BCA command 0x%08x (Buffer 0x%08x, 0x%x)", command_0,
              output_address, output_length);
    break;

  // Probably only used by Wii
  case DVDLowReportKey:
    INFO_LOG(DVDINTERFACE, "DVDLowReportKey");
    // Does not work on retail discs/drives
    // Retail games send this command to see if they are running on real retail hw
    s_error_code = ERROR_READY | ERROR_INV_CMD;
    interrupt_type = INT_BRKINT;
    break;

  // DMA Read from Disc. Only seems to be used through direct access, not WII_IPC
  case 0xA8:
    switch (command_0 & 0xFF)
    {
    case 0x00:  // Read Sector
    {
      u64 iDVDOffset = (u64)command_1 << 2;

      INFO_LOG(DVDINTERFACE, "Read: DVDOffset=%08" PRIx64
                             ", DMABuffer = %08x, SrcLength = %08x, DMALength = %08x",
               iDVDOffset, output_address, command_2, output_length);

      if (GCAM)
      {
        if (iDVDOffset & 0x80000000)  // read request to hardware buffer
        {
          switch (iDVDOffset)
          {
          case 0x80000000:
            ERROR_LOG(DVDINTERFACE, "GC-AM: READ MEDIA BOARD STATUS (80000000)");
            for (u32 i = 0; i < output_length; i += 4)
              Memory::Write_U32(0, output_address + i);
            break;
          case 0x80000040:
            ERROR_LOG(DVDINTERFACE, "GC-AM: READ MEDIA BOARD STATUS (2) (80000040)");
            for (u32 i = 0; i < output_length; i += 4)
              Memory::Write_U32(~0, output_address + i);
            Memory::Write_U32(0x00000020, output_address);      // DIMM SIZE, LE
            Memory::Write_U32(0x4743414D, output_address + 4);  // GCAM signature
            break;
          case 0x80000120:
            ERROR_LOG(DVDINTERFACE, "GC-AM: READ FIRMWARE STATUS (80000120)");
            for (u32 i = 0; i < output_length; i += 4)
              Memory::Write_U32(0x01010101, output_address + i);
            break;
          case 0x80000140:
            ERROR_LOG(DVDINTERFACE, "GC-AM: READ FIRMWARE STATUS (80000140)");
            for (u32 i = 0; i < output_length; i += 4)
              Memory::Write_U32(0x01010101, output_address + i);
            break;
          case 0x84000020:
            ERROR_LOG(DVDINTERFACE, "GC-AM: READ MEDIA BOARD STATUS (1) (84000020)");
            for (u32 i = 0; i < output_length; i += 4)
              Memory::Write_U32(0x00000000, output_address + i);
            break;
          default:
            ERROR_LOG(DVDINTERFACE, "GC-AM: UNKNOWN MEDIA BOARD LOCATION %" PRIx64, iDVDOffset);
            break;
          }
          break;
        }
        else if ((iDVDOffset == 0x1f900000) || (iDVDOffset == 0x1f900020))
        {
          ERROR_LOG(DVDINTERFACE, "GC-AM: READ MEDIA BOARD COMM AREA (1f900020)");
          u8* source = s_media_buffer + iDVDOffset - 0x1f900000;
          Memory::CopyToEmu(output_address, source, output_length);
          for (u32 i = 0; i < output_length; i += 4)
            ERROR_LOG(DVDINTERFACE, "GC-AM: %08x", Memory::Read_U32(output_address + i));
          break;
        }
      }

      command_handled_by_thread =
          ExecuteReadCommand(iDVDOffset, output_address, command_2, output_length, false,
                             reply_to_ios, &interrupt_type, &ticks_until_completion);
    }
    break;

    case 0x40:  // Read DiscID
      INFO_LOG(DVDINTERFACE, "Read DiscID %08x", Memory::Read_U32(output_address));
      command_handled_by_thread =
          ExecuteReadCommand(0, output_address, 0x20, output_length, false, reply_to_ios,
                             &interrupt_type, &ticks_until_completion);
      break;

    default:
      ERROR_LOG(DVDINTERFACE, "Unknown read subcommand: %08x", command_0);
      break;
    }
    break;

  // GC-AM only
  case 0xAA:
    if (GCAM)
    {
      ERROR_LOG(DVDINTERFACE, "GC-AM: 0xAA, DMABuffer=%08x, DMALength=%08x", output_address,
                output_length);
      u64 iDVDOffset = (u64)command_1 << 2;
      u32 len = output_length;
      s64 offset = iDVDOffset - 0x1F900000;
      /*
      if (iDVDOffset == 0x84800000)
      {
        ERROR_LOG(DVDINTERFACE, "Firmware upload");
      }
      else*/
      if ((offset < 0) || ((offset + len) > 0x40) || len > 0x40)
      {
        u32 addr = output_address;
        if (iDVDOffset == 0x84800000)
        {
          ERROR_LOG(DVDINTERFACE, "FIRMWARE UPLOAD");
        }
        else
        {
          ERROR_LOG(DVDINTERFACE, "ILLEGAL MEDIA WRITE");
        }

        while (len >= 4)
        {
          ERROR_LOG(DVDINTERFACE, "GC-AM Media Board WRITE (0xAA): %08" PRIx64 ": %08x", iDVDOffset,
                    Memory::Read_U32(addr));
          addr += 4;
          len -= 4;
          iDVDOffset += 4;
        }
      }
      else
      {
        u32 addr = s_DIMAR.Address;
        Memory::CopyFromEmu(s_media_buffer + offset, addr, len);
        while (len >= 4)
        {
          ERROR_LOG(DVDINTERFACE, "GC-AM Media Board WRITE (0xAA): %08" PRIx64 ": %08x", iDVDOffset,
                    Memory::Read_U32(addr));
          addr += 4;
          len -= 4;
          iDVDOffset += 4;
        }
      }
    }
    break;

  // Seems to be used by both GC and Wii
  case DVDLowSeek:
    if (!GCAM)
    {
      // Currently unimplemented
      INFO_LOG(DVDINTERFACE, "Seek: offset=%09" PRIx64 " (ignoring)", (u64)command_1 << 2);
    }
    else
    {
      memset(s_media_buffer, 0, 0x20);
      s_media_buffer[0] = s_media_buffer[0x20];  // ID
      s_media_buffer[2] = s_media_buffer[0x22];
      s_media_buffer[3] = s_media_buffer[0x23] | 0x80;
      int cmd = (s_media_buffer[0x23] << 8) | s_media_buffer[0x22];
      ERROR_LOG(DVDINTERFACE, "GC-AM: execute buffer, cmd=%04x", cmd);
      switch (cmd)
      {
      case 0x00:
        s_media_buffer[4] = 1;
        break;
      case 0x1:
        s_media_buffer[7] = 0x20;  // DIMM Size
        break;
      case 0x100:
      {
        // urgh
        static int percentage = 0;
        static int status = 0;
        percentage++;
        if (percentage > 100)
        {
          status++;
          percentage = 0;
        }
        s_media_buffer[4] = status;
        /* status:
        0 - "Initializing media board. Please wait.."
        1 - "Checking network. Please wait..."
        2 - "Found a system disc. Insert a game disc"
        3 - "Testing a game program. %d%%"
        4 - "Loading a game program. %d%%"
        5  - go
        6  - error xx
        */
        s_media_buffer[8] = percentage;
        s_media_buffer[4] = 0x05;
        s_media_buffer[8] = 0x64;
        break;
      }
      case 0x101:
        s_media_buffer[4] = 3;  // version
        s_media_buffer[5] = 3;
        s_media_buffer[6] = 1;  // xxx
        s_media_buffer[8] = 1;
        s_media_buffer[16] = 0xFF;
        s_media_buffer[17] = 0xFF;
        s_media_buffer[18] = 0xFF;
        s_media_buffer[19] = 0xFF;
        break;
      case 0x102:               // get error code
        s_media_buffer[4] = 1;  // 0: download incomplete (31), 1: corrupted, other error 1
        s_media_buffer[5] = 0;
        break;
      case 0x103:
        memcpy(s_media_buffer + 4, "A89E27A50364511", 15);  // serial
        break;
#if 0
			case 0x301: // unknown
				memcpy(s_media_buffer + 4, s_media_buffer + 0x24, 0x1c);
				break;
			case 0x302:
				break;
#endif
      default:
        ERROR_LOG(DVDINTERFACE, "GC-AM: execute buffer (unknown)");
        break;
      }
      memset(s_media_buffer + 0x20, 0, 0x20);
      WriteImmediate(0x66556677, output_address, reply_to_ios);  // just a random value that works.
    }
    break;

  // Probably only used by Wii
  case DVDLowReadDvd:
    ERROR_LOG(DVDINTERFACE, "DVDLowReadDvd");
    break;
  // Probably only used by Wii
  case DVDLowReadDvdConfig:
    ERROR_LOG(DVDINTERFACE, "DVDLowReadDvdConfig");
    break;
  // Probably only used by Wii
  case DVDLowStopLaser:
    ERROR_LOG(DVDINTERFACE, "DVDLowStopLaser");
    break;
  // Probably only used by Wii
  case DVDLowOffset:
    ERROR_LOG(DVDINTERFACE, "DVDLowOffset");
    break;
  // Probably only used by Wii
  case DVDLowReadDiskBca:
    WARN_LOG(DVDINTERFACE, "DVDLowReadDiskBca");
    Memory::Write_U32(1, output_address + 0x30);
    break;
  // Probably only used by Wii
  case DVDLowRequestDiscStatus:
    ERROR_LOG(DVDINTERFACE, "DVDLowRequestDiscStatus");
    break;
  // Probably only used by Wii
  case DVDLowRequestRetryNumber:
    ERROR_LOG(DVDINTERFACE, "DVDLowRequestRetryNumber");
    break;
  // Probably only used by Wii
  case DVDLowSetMaximumRotation:
    ERROR_LOG(DVDINTERFACE, "DVDLowSetMaximumRotation");
    break;
  // Probably only used by Wii
  case DVDLowSerMeasControl:
    ERROR_LOG(DVDINTERFACE, "DVDLowSerMeasControl");
    break;

  // Used by both GC and Wii
  case DVDLowRequestError:
    INFO_LOG(DVDINTERFACE, "Requesting error... (0x%08x)", s_error_code);
    WriteImmediate(s_error_code, output_address, reply_to_ios);
    s_error_code = 0;
    break;

  // Audio Stream (Immediate). Only seems to be used by some GC games
  // (command_0 >> 16) & 0xFF = Subcommand
  // command_1 << 2           = Offset on disc
  // command_2                = Length of the stream
  case 0xE1:
  {
    u8 cancel_stream = (command_0 >> 16) & 0xFF;
    if (cancel_stream)
    {
      s_stop_at_track_end = false;
      s_stream = false;
      s_audio_position = 0;
      s_next_start = 0;
      s_next_length = 0;
      s_current_start = 0;
      s_current_length = 0;
    }
    else
    {
      if ((command_1 == 0) && (command_2 == 0))
      {
        s_stop_at_track_end = true;
      }
      else if (!s_stop_at_track_end)
      {
        // Setting s_next_start (a u32) like this discards two bits,
        // but GC games can't be 4 GiB big, so it shouldn't matter
        s_next_start = command_1 << 2;
        s_next_length = command_2;
        if (!s_stream)
        {
          s_current_start = s_next_start;
          s_current_length = s_next_length;
          s_audio_position = s_current_start;
          StreamADPCM::InitFilter();
          s_stream = true;
        }
      }
    }

    INFO_LOG(DVDINTERFACE, "(Audio) Stream cmd: %08x offset: %08" PRIx64 " length: %08x", command_0,
             (u64)command_1 << 2, command_2);
  }
  break;

  // Request Audio Status (Immediate). Only seems to be used by some GC games
  case 0xE2:
  {
    switch (command_0 >> 16 & 0xFF)
    {
    case 0x00:  // Returns streaming status
      INFO_LOG(DVDINTERFACE, "(Audio): Stream Status: Request Audio status AudioPos:%08x/%08x "
                             "CurrentStart:%08x CurrentLength:%08x",
               s_audio_position, s_current_start + s_current_length, s_current_start,
               s_current_length);
      WriteImmediate(s_stream ? 1 : 0, output_address, reply_to_ios);
      break;
    case 0x01:  // Returns the current offset
      INFO_LOG(DVDINTERFACE, "(Audio): Stream Status: Request Audio status AudioPos:%08x",
               s_audio_position);
      WriteImmediate(s_audio_position >> 2, output_address, reply_to_ios);
      break;
    case 0x02:  // Returns the start offset
      INFO_LOG(DVDINTERFACE, "(Audio): Stream Status: Request Audio status CurrentStart:%08x",
               s_current_start);
      WriteImmediate(s_current_start >> 2, output_address, reply_to_ios);
      break;
    case 0x03:  // Returns the total length
      INFO_LOG(DVDINTERFACE, "(Audio): Stream Status: Request Audio status CurrentLength:%08x",
               s_current_length);
      WriteImmediate(s_current_length >> 2, output_address, reply_to_ios);
      break;
    default:
      WARN_LOG(DVDINTERFACE, "(Audio): Subcommand: %02x  Request Audio status %s",
               command_0 >> 16 & 0xFF, s_stream ? "on" : "off");
      break;
    }
  }
  break;

  case DVDLowStopMotor:
    INFO_LOG(DVDINTERFACE, "DVDLowStopMotor %s %s", command_1 ? "eject" : "",
             command_2 ? "kill!" : "");

    if (command_1 && !command_2)
      EjectDiscCallback(0, 0);
    break;

  // DVD Audio Enable/Disable (Immediate). GC uses this, and apparently Wii also does...?
  case DVDLowAudioBufferConfig:
    // For more information: http://www.crazynation.org/GC/GC_DD_TECH/GCTech.htm (dead link?)
    //
    // Upon Power up or reset , 2 commands must be issued for proper use of audio streaming:
    // DVDReadDiskID A8000040,00000000,00000020
    // DVDLowAudioBufferConfig E4xx00yy,00000000,00000020
    //
    // xx=byte 8 [0 or 1] from the disk header retrieved from DVDReadDiskID
    // yy=0 (if xx=0) or 0xA (if xx=1)

    if ((command_0 >> 16) & 0xFF)
    {
      // TODO: What is this actually supposed to do?
      s_stream = true;
      WARN_LOG(DVDINTERFACE, "(Audio): Audio enabled");
    }
    else
    {
      // TODO: What is this actually supposed to do?
      s_stream = false;
      WARN_LOG(DVDINTERFACE, "(Audio): Audio disabled");
    }
    break;

  // yet another (GC?) command we prolly don't care about
  case 0xEE:
    INFO_LOG(DVDINTERFACE, "SetStatus");
    break;

  // Debug commands; see yagcd. We don't really care
  // NOTE: commands to stream data will send...a raw data stream
  // This will appear as unknown commands, unless the check is re-instated to catch such data.
  // Can probably only be used through direct access
  case 0xFE:
    ERROR_LOG(DVDINTERFACE, "Unsupported DVD Drive debug command 0x%08x", command_0);
    break;

  // Unlock Commands. 1: "MATSHITA" 2: "DVD-GAME"
  // Just for fun
  // Can probably only be used through direct access
  case 0xFF:
  {
    if (command_0 == 0xFF014D41 && command_1 == 0x54534849 && command_2 == 0x54410200)
    {
      INFO_LOG(DVDINTERFACE, "Unlock test 1 passed");
    }
    else if (command_0 == 0xFF004456 && command_1 == 0x442D4741 && command_2 == 0x4D450300)
    {
      INFO_LOG(DVDINTERFACE, "Unlock test 2 passed");
    }
    else
    {
      INFO_LOG(DVDINTERFACE, "Unlock test failed");
    }
  }
  break;

  default:
    ERROR_LOG(DVDINTERFACE, "Unknown command 0x%08x (Buffer 0x%08x, 0x%x)", command_0,
              output_address, output_length);
    PanicAlertT("Unknown DVD command %08x - fatal error", command_0);
    break;
  }

  // The command will finish executing after a delay
  // to simulate the speed of a real disc drive
  if (!command_handled_by_thread)
  {
    u64 userdata = (static_cast<u64>(reply_to_ios) << 32) + static_cast<u32>(interrupt_type);
    CoreTiming::ScheduleEvent((int)ticks_until_completion, s_finish_executing_command, userdata);
  }
}

void FinishExecutingCommandCallback(u64 userdata, s64 cycles_late)
{
  bool reply_to_ios = userdata >> 32 != 0;
  DIInterruptType interrupt_type = static_cast<DIInterruptType>(userdata & 0xFFFFFFFF);
  FinishExecutingCommand(reply_to_ios, interrupt_type);
}

void FinishExecutingCommand(bool reply_to_ios, DIInterruptType interrupt_type)
{
  if (reply_to_ios)
  {
    std::shared_ptr<IWII_IPC_HLE_Device> di = WII_IPC_HLE_Interface::GetDeviceByName("/dev/di");
    if (di)
      std::static_pointer_cast<CWII_IPC_HLE_Device_di>(di)->FinishIOCtl(interrupt_type);

    // If di == nullptr, IOS was probably shut down, so the command shouldn't be completed
  }
  else if (s_DICR.TSTART)
  {
    s_DICR.TSTART = 0;
    s_DILENGTH.Length = 0;
    GenerateDIInterrupt(interrupt_type);
  }
}

// Simulates the timing aspects of reading data from a disc.
// Returns the amount of ticks needed to finish executing the command,
// and sets some state that is used the next time this function runs.
u64 SimulateDiscReadTime(u64 offset, u32 length)
{
  // The drive buffers 1 MiB (?) of data after every read request;
  // if a read request is covered by this buffer (or if it's
  // faster to wait for the data to be buffered), the drive
  // doesn't seek; it returns buffered data.  Data can be
  // transferred from the buffer at up to 16 MiB/s.
  //
  // If the drive has to seek, the time this takes varies a lot.
  // A short seek is around 50 ms; a long seek is around 150 ms.
  // However, the time isn't purely dependent on the distance; the
  // pattern of previous seeks seems to matter in a way I'm
  // not sure how to explain.
  //
  // Metroid Prime is a good example of a game that's sensitive to
  // all of these details; if there isn't enough latency in the
  // right places, doors open too quickly, and if there's too
  // much latency in the wrong places, the video before the
  // save-file select screen lags.
  //
  // For now, just use a very rough approximation: 50 ms seek
  // for reads outside 1 MiB, accelerated reads within 1 MiB.
  // We can refine this if someone comes up with a more complete
  // model for seek times.

  u64 current_time = CoreTiming::GetTicks();
  u64 ticks_until_completion;

  // Number of ticks it takes to seek and read directly from the disk.
  u64 disk_read_duration = CalculateRawDiscReadTime(offset, length) +
                           SystemTimers::GetTicksPerSecond() / 1000 * DISC_ACCESS_TIME_MS;

  // Assume unbuffered read if the read we are performing asks for data >
  // 1MB past the end of the last read *or* asks for data before the last
  // read. It assumes the buffer is only used when reading small amounts
  // forward.
  if (offset + length > s_last_read_offset + 1024 * 1024 || offset < s_last_read_offset)
  {
    // No buffer; just use the simple seek time + read time.
    DEBUG_LOG(DVDINTERFACE, "Seeking %" PRId64 " bytes", s64(offset) - s64(s_last_read_offset));
    ticks_until_completion = disk_read_duration;
    s_last_read_time = current_time + ticks_until_completion;
  }
  else
  {
    // Possibly buffered; use the buffer if it saves time.
    // It's not proven that the buffer actually behaves like this, but
    // it appears to be a decent approximation.

    // Time at which the buffer will contain the data we need.
    u64 buffer_fill_time =
        s_last_read_time +
        CalculateRawDiscReadTime(s_last_read_offset, offset + length - s_last_read_offset);
    // Number of ticks it takes to transfer the data from the buffer to memory.
    u64 buffer_read_duration = length * (SystemTimers::GetTicksPerSecond() / BUFFER_TRANSFER_RATE);

    if (current_time > buffer_fill_time)
    {
      DEBUG_LOG(DVDINTERFACE, "Fast buffer read at %" PRIx64, offset);
      ticks_until_completion = buffer_read_duration;
      s_last_read_time = buffer_fill_time;
    }
    else if (current_time + disk_read_duration > buffer_fill_time)
    {
      DEBUG_LOG(DVDINTERFACE, "Slow buffer read at %" PRIx64, offset);
      ticks_until_completion = std::max(buffer_fill_time - current_time, buffer_read_duration);
      s_last_read_time = buffer_fill_time;
    }
    else
    {
      DEBUG_LOG(DVDINTERFACE, "Short seek %" PRId64 " bytes",
                s64(offset) - s64(s_last_read_offset));
      ticks_until_completion = disk_read_duration;
      s_last_read_time = current_time + ticks_until_completion;
    }
  }

  s_last_read_offset = ROUND_DOWN(offset + length - 2048, 2048);

  return ticks_until_completion;
}

// Returns the number of ticks it takes to read an amount of
// data from a disc, ignoring factors such as seek times.
// The result will be negative if the length is negative.
s64 CalculateRawDiscReadTime(u64 offset, s64 length)
{
  // The speed will be calculated using the average offset. This is a bit
  // inaccurate since the speed doesn't increase linearly with the offset,
  // but since reads only span a small part of the disc, it's insignificant.
  u64 average_offset = offset + (length / 2);

  // Here, addresses on the second layer of Wii discs are replaced with equivalent
  // addresses on the first layer so that the speed calculation works correctly.
  // This is wrong for reads spanning two layers, but those should be rare.
  average_offset %= WII_DISC_LAYER_SIZE;

  // The area on the disc between position 1 and the arbitrary position X is:
  // LOCATION_X_SPEED * LOCATION_X_SPEED * pi - AREA_UP_TO_LOCATION_1
  //
  // The number of bytes between position 1 and position X is:
  // LOCATION_X_OFFSET - LOCATION_1_OFFSET
  //
  // This means that the following equation is true:
  // (LOCATION_X_SPEED * LOCATION_X_SPEED * pi - AREA_UP_TO_LOCATION_1) *
  // BYTES_PER_AREA_UNIT = LOCATION_X_OFFSET - LOCATION_1_OFFSET
  //
  // Solving this equation for LOCATION_X_SPEED results in this:
  // LOCATION_X_SPEED = sqrt(((LOCATION_X_OFFSET - LOCATION_1_OFFSET) /
  // BYTES_PER_AREA_UNIT + AREA_UP_TO_LOCATION_1) / pi)
  //
  // Note that the speed at a track (in bytes per second) is the same as
  // the radius of that track because of the length unit used.
  double speed;
  if (s_inserted_volume->GetVolumeType() == DiscIO::IVolume::WII_DISC)
  {
    speed = std::sqrt(((average_offset - WII_DISC_LOCATION_1_OFFSET) / WII_BYTES_PER_AREA_UNIT +
                       WII_DISC_AREA_UP_TO_LOCATION_1) /
                      PI);
  }
  else
  {
    speed = std::sqrt(((average_offset - GC_DISC_LOCATION_1_OFFSET) / GC_BYTES_PER_AREA_UNIT +
                       GC_DISC_AREA_UP_TO_LOCATION_1) /
                      PI);
  }
  DEBUG_LOG(DVDINTERFACE, "Disc speed: %f MiB/s", speed / 1024 / 1024);

  return (s64)(SystemTimers::GetTicksPerSecond() / speed * length);
}

}  // namespace
