// Copyright 2008 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#include "Core/HW/DVD/DVDInterface.h"

#include <algorithm>
#include <memory>
#include <optional>
#include <string>
#include <vector>

#include "AudioCommon/AudioCommon.h"

#include "Common/Align.h"
#include "Common/BitField.h"
#include "Common/ChunkFile.h"
#include "Common/CommonTypes.h"
#include "Common/Config/Config.h"
#include "Common/Logging/Log.h"

#include "Core/Config/MainSettings.h"
#include "Core/CoreTiming.h"
#include "Core/DolphinAnalytics.h"
#include "Core/HW/AudioInterface.h"
#include "Core/HW/DVD/DVDMath.h"
#include "Core/HW/DVD/DVDThread.h"
#include "Core/HW/EXI/EXI_DeviceIPL.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/IOS/DI/DI.h"
#include "Core/IOS/IOS.h"
#include "Core/Movie.h"

#include "DiscIO/Blob.h"
#include "DiscIO/DiscUtils.h"
#include "DiscIO/Enums.h"
#include "DiscIO/VolumeDisc.h"
#include "DiscIO/VolumeWii.h"

#include "VideoCommon/OnScreenDisplay.h"

// The minimum time it takes for the DVD drive to process a command (in microseconds)
constexpr u64 MINIMUM_COMMAND_LATENCY_US = 300;

// The time it takes for a read command to start (in microseconds)
constexpr u64 READ_COMMAND_LATENCY_US = 600;

// The size of the streaming buffer.
constexpr u64 STREAMING_BUFFER_SIZE = 1024 * 1024;

// A single DVD disc sector
constexpr u64 DVD_SECTOR_SIZE = 0x800;

// The minimum amount that a drive will read
constexpr u64 DVD_ECC_BLOCK_SIZE = 16 * DVD_SECTOR_SIZE;

// Rate the drive can transfer data to main memory, given the data
// is already buffered. Measured in bytes per second.
constexpr u64 BUFFER_TRANSFER_RATE = 32 * 1024 * 1024;

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

// DI Status Register
union UDISR
{
  u32 Hex = 0;

  BitField<0, 1, u32> BREAK;      // Stop the Device + Interrupt
  BitField<1, 1, u32> DEINTMASK;  // Access Device Error Int Mask
  BitField<2, 1, u32> DEINT;      // Access Device Error Int
  BitField<3, 1, u32> TCINTMASK;  // Transfer Complete Int Mask
  BitField<4, 1, u32> TCINT;      // Transfer Complete Int
  BitField<5, 1, u32> BRKINTMASK;
  BitField<6, 1, u32> BRKINT;  // w 1: clear brkint
  BitField<7, 25, u32> reserved;

  UDISR() = default;
  explicit UDISR(u32 hex) : Hex{hex} {}
};

// DI Cover Register
union UDICVR
{
  u32 Hex = 0;

  BitField<0, 1, u32> CVR;         // 0: Cover closed  1: Cover open
  BitField<1, 1, u32> CVRINTMASK;  // 1: Interrupt enabled
  BitField<2, 1, u32> CVRINT;      // r 1: Interrupt requested w 1: Interrupt clear
  BitField<3, 29, u32> reserved;

  UDICVR() = default;
  explicit UDICVR(u32 hex) : Hex{hex} {}
};

// DI DMA Control Register
union UDICR
{
  u32 Hex = 0;

  BitField<0, 1, u32> TSTART;  // w:1 start   r:0 ready
  BitField<1, 1, u32> DMA;     // 1: DMA Mode
                               // 0: Immediate Mode (can only do Access Register Command)
  BitField<2, 1, u32> RW;      // 0: Read Command (DVD to Memory)  1: Write Command (Memory to DVD)
  BitField<3, 29, u32> reserved;
};

// DI Config Register
union UDICFG
{
  u32 Hex = 0;

  BitField<0, 8, u32> CONFIG;
  BitField<8, 24, u32> reserved;

  UDICFG() = default;
  explicit UDICFG(u32 hex) : Hex{hex} {}
};

// STATE_TO_SAVE

// Hardware registers
static UDISR s_DISR;
static UDICVR s_DICVR;
static u32 s_DICMDBUF[3];
static u32 s_DIMAR;
static u32 s_DILENGTH;
static UDICR s_DICR;
static u32 s_DIIMMBUF;
static UDICFG s_DICFG;

static StreamADPCM::ADPCMDecoder s_adpcm_decoder;

// DTK
static bool s_stream = false;
static bool s_stop_at_track_end = false;
static u64 s_audio_position;
static u64 s_current_start;
static u32 s_current_length;
static u64 s_next_start;
static u32 s_next_length;
static u32 s_pending_samples;
static bool s_enable_dtk = false;
static u8 s_dtk_buffer_length = 0;  // TODO: figure out how this affects the regular buffer

// Disc drive state
static DriveState s_drive_state;
static DriveError s_error_code;
static u64 s_disc_end_offset;

// Disc drive timing
static u64 s_read_buffer_start_time;
static u64 s_read_buffer_end_time;
static u64 s_read_buffer_start_offset;
static u64 s_read_buffer_end_offset;

// Disc changing
static std::string s_disc_path_to_insert;
static std::vector<std::string> s_auto_disc_change_paths;
static size_t s_auto_disc_change_index;

// Events
static CoreTiming::EventType* s_finish_executing_command;
static CoreTiming::EventType* s_auto_change_disc;
static CoreTiming::EventType* s_eject_disc;
static CoreTiming::EventType* s_insert_disc;

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

static void SetLidOpen();

static void UpdateInterrupts();
static void GenerateDIInterrupt(DIInterruptType dvd_interrupt);

static bool ExecuteReadCommand(u64 dvd_offset, u32 output_address, u32 dvd_length,
                               u32 output_length, const DiscIO::Partition& partition,
                               ReplyType reply_type, DIInterruptType* interrupt_type);

static u64 PackFinishExecutingCommandUserdata(ReplyType reply_type, DIInterruptType interrupt_type);

static void ScheduleReads(u64 offset, u32 length, const DiscIO::Partition& partition,
                          u32 output_address, ReplyType reply_type);

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_stream);
  p.Do(s_stop_at_track_end);
  p.Do(s_audio_position);
  p.Do(s_current_start);
  p.Do(s_current_length);
  p.Do(s_next_start);
  p.Do(s_next_length);
  p.Do(s_pending_samples);
  p.Do(s_enable_dtk);
  p.Do(s_dtk_buffer_length);

  p.Do(s_drive_state);
  p.Do(s_error_code);

  p.Do(s_read_buffer_start_time);
  p.Do(s_read_buffer_end_time);
  p.Do(s_read_buffer_start_offset);
  p.Do(s_read_buffer_end_offset);

  p.Do(s_disc_path_to_insert);

  DVDThread::DoState(p);

  s_adpcm_decoder.DoState(p);
}

static size_t ProcessDTKSamples(std::vector<s16>* temp_pcm, const std::vector<u8>& audio_data)
{
  size_t samples_processed = 0;
  size_t bytes_processed = 0;
  while (samples_processed < temp_pcm->size() / 2 && bytes_processed < audio_data.size())
  {
    s_adpcm_decoder.DecodeBlock(&(*temp_pcm)[samples_processed * 2], &audio_data[bytes_processed]);
    for (size_t i = 0; i < StreamADPCM::SAMPLES_PER_BLOCK * 2; ++i)
    {
      // TODO: Fix the mixer so it can accept non-byte-swapped samples.
      s16* sample = &(*temp_pcm)[samples_processed * 2 + i];
      *sample = Common::swap16(*sample);
    }
    samples_processed += StreamADPCM::SAMPLES_PER_BLOCK;
    bytes_processed += StreamADPCM::ONE_BLOCK_SIZE;
  }
  return samples_processed;
}

static u32 AdvanceDTK(u32 maximum_samples, u32* samples_to_process)
{
  u32 bytes_to_process = 0;
  *samples_to_process = 0;
  while (*samples_to_process < maximum_samples)
  {
    if (s_audio_position >= s_current_start + s_current_length)
    {
      DEBUG_LOG_FMT(DVDINTERFACE,
                    "AdvanceDTK: 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;
      }

      s_adpcm_decoder.ResetFilter();
    }

    s_audio_position += StreamADPCM::ONE_BLOCK_SIZE;
    bytes_to_process += StreamADPCM::ONE_BLOCK_SIZE;
    *samples_to_process += StreamADPCM::SAMPLES_PER_BLOCK;
  }

  return bytes_to_process;
}

static void DTKStreamingCallback(DIInterruptType interrupt_type, const std::vector<u8>& audio_data,
                                 s64 cycles_late)
{
  // TODO: Should we use GetAISSampleRate instead of a fixed 48 KHz? The audio mixer is using
  // GetAISSampleRate. (This doesn't affect any actual games, since they all set it to 48 KHz.)
  const u32 sample_rate = AudioInterface::Get48KHzSampleRate();

  // Determine which audio data to read next.
  const u32 maximum_samples = sample_rate / 2000 * 7;  // 3.5 ms of samples
  u64 read_offset = 0;
  u32 read_length = 0;

  if (interrupt_type == DIInterruptType::TCINT)
  {
    // Send audio to the mixer.
    std::vector<s16> temp_pcm(s_pending_samples * 2, 0);
    ProcessDTKSamples(&temp_pcm, audio_data);
    g_sound_stream->GetMixer()->PushStreamingSamples(temp_pcm.data(), s_pending_samples);

    if (s_stream && AudioInterface::IsPlaying())
    {
      read_offset = s_audio_position;
      read_length = AdvanceDTK(maximum_samples, &s_pending_samples);
    }
    else
    {
      read_length = 0;
      s_pending_samples = maximum_samples;
    }
  }
  else
  {
    read_length = 0;
    s_pending_samples = maximum_samples;
  }

  // Read the next chunk of audio data asynchronously.
  s64 ticks_to_dtk = SystemTimers::GetTicksPerSecond() * s64(s_pending_samples) / sample_rate;
  ticks_to_dtk -= cycles_late;
  if (read_length > 0)
  {
    DVDThread::StartRead(read_offset, read_length, DiscIO::PARTITION_NONE, ReplyType::DTK,
                         ticks_to_dtk);
  }
  else
  {
    // There's nothing to read, so using DVDThread is unnecessary.
    u64 userdata = PackFinishExecutingCommandUserdata(ReplyType::DTK, DIInterruptType::TCINT);
    CoreTiming::ScheduleEvent(ticks_to_dtk, s_finish_executing_command, userdata);
  }
}

void Init()
{
  ASSERT(!IsDiscInside());

  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] = 0;
  s_DICMDBUF[1] = 0;
  s_DICMDBUF[2] = 0;
  s_DIMAR = 0;
  s_DILENGTH = 0;
  s_DICR.Hex = 0;
  s_DIIMMBUF = 0;
  s_DICFG.Hex = 0;
  s_DICFG.CONFIG = 1;  // Disable bootrom descrambler

  ResetDrive(false);

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

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

  u64 userdata = PackFinishExecutingCommandUserdata(ReplyType::DTK, DIInterruptType::TCINT);
  CoreTiming::ScheduleEvent(0, s_finish_executing_command, userdata);
}

// Resets state on the MN102 chip in the drive itself, but not the DI registers exposed on the
// emulated device, or any inserted disc.
void ResetDrive(bool spinup)
{
  s_stream = false;
  s_stop_at_track_end = false;
  s_audio_position = 0;
  s_next_start = 0;
  s_next_length = 0;
  s_current_start = 0;
  s_current_length = 0;
  s_pending_samples = 0;
  s_enable_dtk = false;
  s_dtk_buffer_length = 0;

  if (!IsDiscInside())
  {
    // CoverOpened is used when the cover is open;
    // NoMediumPresent is used when the cover is closed but there is no disc.
    // On the Wii, this can only happen if something other than a DVD is inserted into the disc
    // drive (for instance, an audio CD) and only after it attempts to read it.  Otherwise, it will
    // report the cover as opened.
    SetDriveState(DriveState::CoverOpened);
  }
  else if (!spinup)
  {
    // Wii hardware tests indicate that this is used when ejecting and inserting a new disc, or
    // performing a reset without spinup.
    SetDriveState(DriveState::DiscChangeDetected);
  }
  else
  {
    SetDriveState(DriveState::DiscIdNotRead);
  }

  SetDriveError(DriveError::None);

  // The buffer is empty at start
  s_read_buffer_start_offset = 0;
  s_read_buffer_end_offset = 0;
  s_read_buffer_start_time = 0;
  s_read_buffer_end_time = 0;
}

void Shutdown()
{
  DVDThread::Stop();
}

static u64 GetDiscEndOffset(const DiscIO::VolumeDisc& disc)
{
  u64 size = disc.GetSize();

  if (disc.IsSizeAccurate())
  {
    if (size == DiscIO::MINI_DVD_SIZE)
      return DiscIO::MINI_DVD_SIZE;
  }
  else
  {
    size = DiscIO::GetBiggestReferencedOffset(disc);
  }

  const bool should_be_mini_dvd =
      disc.GetVolumeType() == DiscIO::Platform::GameCubeDisc || disc.IsDatelDisc();

  // We always return standard DVD sizes here, not DVD-R sizes.
  // RVT-R (devkit) consoles can't read the extra megabytes there are on RVT-R (DVD-R) discs.
  if (should_be_mini_dvd && size <= DiscIO::MINI_DVD_SIZE)
    return DiscIO::MINI_DVD_SIZE;
  else if (size <= DiscIO::SL_DVD_R_SIZE)
    return DiscIO::SL_DVD_SIZE;
  else
    return DiscIO::DL_DVD_SIZE;
}

void SetDisc(std::unique_ptr<DiscIO::VolumeDisc> disc,
             std::optional<std::vector<std::string>> auto_disc_change_paths = {})
{
  bool had_disc = IsDiscInside();
  bool has_disc = static_cast<bool>(disc);

  if (has_disc)
  {
    s_disc_end_offset = GetDiscEndOffset(*disc);
    if (!disc->IsSizeAccurate())
      WARN_LOG_FMT(DVDINTERFACE, "Unknown disc size, guessing {0} bytes", s_disc_end_offset);

    const DiscIO::BlobReader& blob = disc->GetBlobReader();
    if (!blob.HasFastRandomAccessInBlock() && blob.GetBlockSize() > 0x200000)
    {
      OSD::AddMessage("You are running a disc image with a very large block size.", 60000);
      OSD::AddMessage("This will likely lead to performance problems.", 60000);
      OSD::AddMessage("You can use Dolphin's convert feature to reduce the block size.", 60000);
    }
  }

  if (auto_disc_change_paths)
  {
    ASSERT_MSG(DISCIO, (*auto_disc_change_paths).size() != 1,
               "Cannot automatically change between one disc");

    s_auto_disc_change_paths = *auto_disc_change_paths;
    s_auto_disc_change_index = 0;
  }

  // Assume that inserting a disc requires having an empty disc before
  if (had_disc != has_disc)
    ExpansionInterface::g_rtc_flags[ExpansionInterface::RTCFlag::DiscChanged] = true;

  DVDThread::SetDisc(std::move(disc));
  SetLidOpen();

  ResetDrive(false);
}

bool IsDiscInside()
{
  return DVDThread::HasDisc();
}

static void AutoChangeDiscCallback(u64 userdata, s64 cyclesLate)
{
  AutoChangeDisc();
}

static void EjectDiscCallback(u64 userdata, s64 cyclesLate)
{
  SetDisc(nullptr, {});
}

static void InsertDiscCallback(u64 userdata, s64 cyclesLate)
{
  std::unique_ptr<DiscIO::VolumeDisc> new_disc = DiscIO::CreateDisc(s_disc_path_to_insert);

  if (new_disc)
    SetDisc(std::move(new_disc), {});
  else
    PanicAlertFmtT("The disc that was about to be inserted couldn't be found.");

  s_disc_path_to_insert.clear();
}

// Must only be called on the CPU thread
void EjectDisc(EjectCause cause)
{
  CoreTiming::ScheduleEvent(0, s_eject_disc);
  if (cause == EjectCause::User)
    ExpansionInterface::g_rtc_flags[ExpansionInterface::RTCFlag::EjectButton] = true;
}

// Must only be called on the CPU thread
void ChangeDisc(const std::vector<std::string>& paths)
{
  ASSERT_MSG(DISCIO, !paths.empty(), "Trying to insert an empty list of discs");

  if (paths.size() > 1)
  {
    s_auto_disc_change_paths = paths;
    s_auto_disc_change_index = 0;
  }

  ChangeDisc(paths[0]);
}

// Must only be called on the CPU thread
void ChangeDisc(const std::string& new_path)
{
  if (!s_disc_path_to_insert.empty())
  {
    PanicAlertFmtT("A disc is already about to be inserted.");
    return;
  }

  EjectDisc(EjectCause::User);

  s_disc_path_to_insert = new_path;
  CoreTiming::ScheduleEvent(SystemTimers::GetTicksPerSecond(), s_insert_disc);
  Movie::SignalDiscChange(new_path);

  for (size_t i = 0; i < s_auto_disc_change_paths.size(); ++i)
  {
    if (s_auto_disc_change_paths[i] == new_path)
    {
      s_auto_disc_change_index = i;
      return;
    }
  }

  s_auto_disc_change_paths.clear();
}

// Must only be called on the CPU thread
bool AutoChangeDisc()
{
  if (s_auto_disc_change_paths.empty())
    return false;

  s_auto_disc_change_index = (s_auto_disc_change_index + 1) % s_auto_disc_change_paths.size();
  ChangeDisc(s_auto_disc_change_paths[s_auto_disc_change_index]);
  return true;
}

static void SetLidOpen()
{
  const u32 old_value = s_DICVR.CVR;
  s_DICVR.CVR = IsDiscInside() ? 0 : 1;
  if (s_DICVR.CVR != old_value)
    GenerateDIInterrupt(DIInterruptType::CVRINT);
}

bool UpdateRunningGameMetadata(std::optional<u64> title_id)
{
  if (!DVDThread::HasDisc())
    return false;

  return DVDThread::UpdateRunningGameMetadata(IOS::HLE::DIDevice::GetCurrentPartition(), title_id);
}

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) {
                   const UDISR tmp_status_reg(val);

                   s_DISR.DEINTMASK = tmp_status_reg.DEINTMASK.Value();
                   s_DISR.TCINTMASK = tmp_status_reg.TCINTMASK.Value();
                   s_DISR.BRKINTMASK = tmp_status_reg.BRKINTMASK.Value();
                   s_DISR.BREAK = tmp_status_reg.BREAK.Value();

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

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

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

                   if (s_DISR.BREAK)
                   {
                     DEBUG_ASSERT(0);
                   }

                   UpdateInterrupts();
                 }));

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

                   s_DICVR.CVRINTMASK = tmp_cover_reg.CVRINTMASK.Value();

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

                   UpdateInterrupts();
                 }));

  // Command registers, which have no special logic
  mmio->Register(base | DI_COMMAND_0, MMIO::DirectRead<u32>(&s_DICMDBUF[0]),
                 MMIO::DirectWrite<u32>(&s_DICMDBUF[0]));
  mmio->Register(base | DI_COMMAND_1, MMIO::DirectRead<u32>(&s_DICMDBUF[1]),
                 MMIO::DirectWrite<u32>(&s_DICMDBUF[1]));
  mmio->Register(base | DI_COMMAND_2, MMIO::DirectRead<u32>(&s_DICMDBUF[2]),
                 MMIO::DirectWrite<u32>(&s_DICMDBUF[2]));

  // 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),
                 MMIO::DirectWrite<u32>(&s_DIMAR, ~0x1F));
  mmio->Register(base | DI_DMA_LENGTH_REGISTER, MMIO::DirectRead<u32>(&s_DILENGTH),
                 MMIO::DirectWrite<u32>(&s_DILENGTH, ~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(ReplyType::Interrupt);
                   }
                 }));

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

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

static void UpdateInterrupts()
{
  const bool set_mask =
      (s_DISR.DEINT & s_DISR.DEINTMASK) != 0 || (s_DISR.TCINT & s_DISR.TCINTMASK) != 0 ||
      (s_DISR.BRKINT & s_DISR.BRKINTMASK) != 0 || (s_DICVR.CVRINT & s_DICVR.CVRINTMASK) != 0;

  ProcessorInterface::SetInterrupt(ProcessorInterface::INT_CAUSE_DI, set_mask);

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

static void GenerateDIInterrupt(DIInterruptType dvd_interrupt)
{
  switch (dvd_interrupt)
  {
  case DIInterruptType::DEINT:
    s_DISR.DEINT = true;
    break;
  case DIInterruptType::TCINT:
    s_DISR.TCINT = true;
    break;
  case DIInterruptType::BRKINT:
    s_DISR.BRKINT = true;
    break;
  case DIInterruptType::CVRINT:
    s_DICVR.CVRINT = true;
    break;
  }

  UpdateInterrupts();
}

void SetInterruptEnabled(DIInterruptType interrupt, bool enabled)
{
  switch (interrupt)
  {
  case DIInterruptType::DEINT:
    s_DISR.DEINTMASK = enabled;
    break;
  case DIInterruptType::TCINT:
    s_DISR.TCINTMASK = enabled;
    break;
  case DIInterruptType::BRKINT:
    s_DISR.BRKINTMASK = enabled;
    break;
  case DIInterruptType::CVRINT:
    s_DICVR.CVRINTMASK = enabled;
    break;
  }
}

void ClearInterrupt(DIInterruptType interrupt)
{
  switch (interrupt)
  {
  case DIInterruptType::DEINT:
    s_DISR.DEINT = false;
    break;
  case DIInterruptType::TCINT:
    s_DISR.TCINT = false;
    break;
  case DIInterruptType::BRKINT:
    s_DISR.BRKINT = false;
    break;
  case DIInterruptType::CVRINT:
    s_DICVR.CVRINT = false;
    break;
  }
}

// Checks the drive state to make sure a read-like command can be performed.
// If false is returned, SetDriveError will have been called, and the caller
// should issue a DEINT interrupt.
static bool CheckReadPreconditions()
{
  if (!IsDiscInside())  // Implies CoverOpened or NoMediumPresent
  {
    ERROR_LOG_FMT(DVDINTERFACE, "No disc inside.");
    SetDriveError(DriveError::MediumNotPresent);
    return false;
  }
  if (s_drive_state == DriveState::DiscChangeDetected)
  {
    ERROR_LOG_FMT(DVDINTERFACE, "Disc changed (motor stopped).");
    SetDriveError(DriveError::MediumChanged);
    return false;
  }
  if (s_drive_state == DriveState::MotorStopped)
  {
    ERROR_LOG_FMT(DVDINTERFACE, "Motor stopped.");
    SetDriveError(DriveError::MotorStopped);
    return false;
  }
  if (s_drive_state == DriveState::DiscIdNotRead)
  {
    ERROR_LOG_FMT(DVDINTERFACE, "Disc id not read.");
    SetDriveError(DriveError::NoDiscID);
    return false;
  }
  return true;
}

// Iff false is returned, ScheduleEvent must be used to finish executing the command
static bool ExecuteReadCommand(u64 dvd_offset, u32 output_address, u32 dvd_length,
                               u32 output_length, const DiscIO::Partition& partition,
                               ReplyType reply_type, DIInterruptType* interrupt_type)
{
  if (!CheckReadPreconditions())
  {
    // Disc read fails
    *interrupt_type = DIInterruptType::DEINT;
    return false;
  }
  else
  {
    // Disc read succeeds
    *interrupt_type = DIInterruptType::TCINT;
  }

  if (dvd_length > output_length)
  {
    WARN_LOG_FMT(DVDINTERFACE, "Detected an attempt to read more data from the DVD "
                               "than what fits inside the out buffer. Clamping.");
    dvd_length = output_length;
  }

  // Many Wii games intentionally try to read from an offset which is just past the end of a
  // regular DVD but just before the end of a DVD-R, displaying "Error #001" and failing to boot
  // if the read succeeds, so it's critical that we set the correct error code for such reads.
  // See https://wiibrew.org/wiki//dev/di#0x8D_DVDLowUnencryptedRead for details on Error #001.
  if (dvd_offset + dvd_length > s_disc_end_offset)
  {
    SetDriveError(DriveError::BlockOOB);
    *interrupt_type = DIInterruptType::DEINT;
    return false;
  }

  ScheduleReads(dvd_offset, dvd_length, partition, output_address, reply_type);
  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(ReplyType reply_type)
{
  DIInterruptType interrupt_type = DIInterruptType::TCINT;
  bool command_handled_by_thread = false;

  // DVDLowRequestError needs access to the error code set by the previous command
  if (static_cast<DICommand>(s_DICMDBUF[0] >> 24) != DICommand::RequestError)
    SetDriveError(DriveError::None);

  switch (static_cast<DICommand>(s_DICMDBUF[0] >> 24))
  {
  // Used by both GC and Wii
  case DICommand::Inquiry:
    // (shuffle2) Taken from my Wii
    Memory::Write_U32(0x00000002, s_DIMAR);      // Revision level, device code
    Memory::Write_U32(0x20060526, s_DIMAR + 4);  // Release date
    Memory::Write_U32(0x41000000, s_DIMAR + 8);  // Version

    INFO_LOG_FMT(DVDINTERFACE, "DVDLowInquiry (Buffer {:#010x}, {:#x})", s_DIMAR, s_DILENGTH);
    break;

  // GC-only patched drive firmware command, used by libogc
  case DICommand::Unknown55:
    INFO_LOG_FMT(DVDINTERFACE, "SetExtension");
    SetDriveError(DriveError::InvalidCommand);
    interrupt_type = DIInterruptType::DEINT;
    break;

  // Wii-exclusive
  case DICommand::ReportKey:
    INFO_LOG_FMT(DVDINTERFACE, "DVDLowReportKey");
    // Does not work on retail discs/drives
    // Retail games send this command to see if they are running on real retail hw
    SetDriveError(DriveError::InvalidCommand);
    interrupt_type = DIInterruptType::DEINT;
    break;

  // DMA Read from Disc. Only used through direct access on GC; direct use is prohibited by
  // IOS (which uses it internally)
  case DICommand::Read:
    switch (s_DICMDBUF[0] & 0xFF)
    {
    case 0x00:  // Read Sector
    {
      const u64 dvd_offset = static_cast<u64>(s_DICMDBUF[1]) << 2;

      INFO_LOG_FMT(
          DVDINTERFACE,
          "Read: DVDOffset={:08x}, DMABuffer = {:08x}, SrcLength = {:08x}, DMALength = {:08x}",
          dvd_offset, s_DIMAR, s_DICMDBUF[2], s_DILENGTH);

      if (s_drive_state == DriveState::ReadyNoReadsMade)
        SetDriveState(DriveState::Ready);

      command_handled_by_thread =
          ExecuteReadCommand(dvd_offset, s_DIMAR, s_DICMDBUF[2], s_DILENGTH, DiscIO::PARTITION_NONE,
                             reply_type, &interrupt_type);
    }
    break;

    case 0x40:  // Read DiscID
      INFO_LOG_FMT(DVDINTERFACE, "Read DiscID: buffer {:08x}", s_DIMAR);
      if (s_drive_state == DriveState::DiscIdNotRead)
      {
        SetDriveState(DriveState::ReadyNoReadsMade);
      }
      else if (s_drive_state == DriveState::ReadyNoReadsMade)
      {
        // The first disc ID reading is required before DTK can be configured.
        // If the disc ID is read again (or any other read occurs), it no longer can
        // be configured.
        SetDriveState(DriveState::Ready);
      }

      command_handled_by_thread = ExecuteReadCommand(
          0, s_DIMAR, 0x20, s_DILENGTH, DiscIO::PARTITION_NONE, reply_type, &interrupt_type);
      break;

    default:
      ERROR_LOG_FMT(DVDINTERFACE, "Unknown read subcommand: {:08x}", s_DICMDBUF[0]);
      break;
    }
    break;

  // Used by both GC and Wii
  case DICommand::Seek:
    // Currently unimplemented
    INFO_LOG_FMT(DVDINTERFACE, "Seek: offset={:09x} (ignoring)",
                 static_cast<u64>(s_DICMDBUF[1]) << 2);
    break;

  // Wii-exclusive
  case DICommand::ReadDVDMetadata:
    switch ((s_DICMDBUF[0] >> 16) & 0xFF)
    {
    case 0:
      ERROR_LOG_FMT(DVDINTERFACE, "DVDLowReadDvdPhysical");
      break;
    case 1:
      ERROR_LOG_FMT(DVDINTERFACE, "DVDLowReadDvdCopyright");
      break;
    case 2:
      ERROR_LOG_FMT(DVDINTERFACE, "DVDLowReadDvdDiscKey");
      break;
    default:
      ERROR_LOG_FMT(DVDINTERFACE, "Unknown 0xAD subcommand in {:08x}", s_DICMDBUF[0]);
      break;
    }
    SetDriveError(DriveError::InvalidCommand);
    interrupt_type = DIInterruptType::DEINT;
    break;
  // Wii-exclusive
  case DICommand::ReadDVD:
    ERROR_LOG_FMT(DVDINTERFACE, "DVDLowReadDvd");
    SetDriveError(DriveError::InvalidCommand);
    interrupt_type = DIInterruptType::DEINT;
    break;
  // Wii-exclusive
  case DICommand::ReadDVDConfig:
    ERROR_LOG_FMT(DVDINTERFACE, "DVDLowReadDvdConfig");
    SetDriveError(DriveError::InvalidCommand);
    interrupt_type = DIInterruptType::DEINT;
    break;
  // Wii-exclusive
  case DICommand::StopLaser:
    ERROR_LOG_FMT(DVDINTERFACE, "DVDLowStopLaser");
    DolphinAnalytics::Instance().ReportGameQuirk(GameQuirk::USES_DVD_LOW_STOP_LASER);
    SetDriveError(DriveError::InvalidCommand);
    interrupt_type = DIInterruptType::DEINT;
    break;
  // Wii-exclusive
  case DICommand::Offset:
    ERROR_LOG_FMT(DVDINTERFACE, "DVDLowOffset");
    DolphinAnalytics::Instance().ReportGameQuirk(GameQuirk::USES_DVD_LOW_OFFSET);
    SetDriveError(DriveError::InvalidCommand);
    interrupt_type = DIInterruptType::DEINT;
    break;
  // Wii-exclusive
  case DICommand::ReadBCA:
    WARN_LOG_FMT(DVDINTERFACE, "DVDLowReadDiskBca - supplying dummy data to appease NSMBW");
    DolphinAnalytics::Instance().ReportGameQuirk(GameQuirk::USES_DVD_LOW_READ_DISK_BCA);
    // NSMBW checks that the first 0x33 bytes of the BCA are 0, then it expects a 1.
    // Most (all?) other games have 0x34 0's at the start of the BCA, but don't actually
    // read it.  NSMBW doesn't care about the other 12 bytes (which contain manufacturing data?)

    // TODO: Read the .bca file that cleanrip generates, if it exists
    // Memory::CopyToEmu(output_address, bca_data, 0x40);
    Memory::Memset(s_DIMAR, 0, 0x40);
    Memory::Write_U8(1, s_DIMAR + 0x33);
    break;
  // Wii-exclusive
  case DICommand::RequestDiscStatus:
    ERROR_LOG_FMT(DVDINTERFACE, "DVDLowRequestDiscStatus");
    DolphinAnalytics::Instance().ReportGameQuirk(GameQuirk::USES_DVD_LOW_REQUEST_DISC_STATUS);
    SetDriveError(DriveError::InvalidCommand);
    interrupt_type = DIInterruptType::DEINT;
    break;
  // Wii-exclusive
  case DICommand::RequestRetryNumber:
    ERROR_LOG_FMT(DVDINTERFACE, "DVDLowRequestRetryNumber");
    DolphinAnalytics::Instance().ReportGameQuirk(GameQuirk::USES_DVD_LOW_REQUEST_RETRY_NUMBER);
    SetDriveError(DriveError::InvalidCommand);
    interrupt_type = DIInterruptType::DEINT;
    break;
  // Wii-exclusive
  case DICommand::SetMaximumRotation:
    ERROR_LOG_FMT(DVDINTERFACE, "DVDLowSetMaximumRotation");
    SetDriveError(DriveError::InvalidCommand);
    interrupt_type = DIInterruptType::DEINT;
    break;
  // Wii-exclusive
  case DICommand::SerMeasControl:
    ERROR_LOG_FMT(DVDINTERFACE, "DVDLowSerMeasControl");
    DolphinAnalytics::Instance().ReportGameQuirk(GameQuirk::USES_DVD_LOW_SER_MEAS_CONTROL);
    SetDriveError(DriveError::InvalidCommand);
    interrupt_type = DIInterruptType::DEINT;
    break;

  // Used by both GC and Wii
  case DICommand::RequestError:
  {
    u32 drive_state;
    if (s_drive_state == DriveState::Ready)
      drive_state = 0;
    else
      drive_state = static_cast<u32>(s_drive_state) - 1;

    const u32 result = (drive_state << 24) | static_cast<u32>(s_error_code);
    INFO_LOG_FMT(DVDINTERFACE, "Requesting error... ({:#010x})", result);
    s_DIIMMBUF = result;
    SetDriveError(DriveError::None);
    break;
  }

  // Audio Stream (Immediate). Only used by some GC games, but does exist on the Wii
  // (command_0 >> 16) & 0xFF = Subcommand
  // command_1 << 2           = Offset on disc
  // command_2                = Length of the stream
  case DICommand::AudioStream:
  {
    if (!CheckReadPreconditions())
    {
      ERROR_LOG_FMT(DVDINTERFACE, "Cannot play audio (command {:08x})", s_DICMDBUF[0]);
      interrupt_type = DIInterruptType::DEINT;
      break;
    }
    if (!s_enable_dtk)
    {
      ERROR_LOG_FMT(
          DVDINTERFACE,
          "Attempted to change playing audio while audio is disabled!  ({:08x} {:08x} {:08x})",
          s_DICMDBUF[0], s_DICMDBUF[1], s_DICMDBUF[2]);
      SetDriveError(DriveError::NoAudioBuf);
      interrupt_type = DIInterruptType::DEINT;
      break;
    }

    if (s_drive_state == DriveState::ReadyNoReadsMade)
      SetDriveState(DriveState::Ready);

    switch ((s_DICMDBUF[0] >> 16) & 0xFF)
    {
    case 0x00:
    {
      const u64 offset = static_cast<u64>(s_DICMDBUF[1]) << 2;
      const u32 length = s_DICMDBUF[2];
      INFO_LOG_FMT(DVDINTERFACE, "(Audio) Start stream: offset: {:08x} length: {:08x}", offset,
                   length);

      if ((offset == 0) && (length == 0))
      {
        s_stop_at_track_end = true;
      }
      else if (!s_stop_at_track_end)
      {
        s_next_start = offset;
        s_next_length = length;
        if (!s_stream)
        {
          s_current_start = s_next_start;
          s_current_length = s_next_length;
          s_audio_position = s_current_start;
          s_adpcm_decoder.ResetFilter();
          s_stream = true;
        }
      }
      break;
    }
    case 0x01:
      INFO_LOG_FMT(DVDINTERFACE, "(Audio) Stop stream");
      s_stop_at_track_end = false;
      s_stream = false;
      break;
    default:
      ERROR_LOG_FMT(DVDINTERFACE, "Invalid audio command!  ({:08x} {:08x} {:08x})", s_DICMDBUF[0],
                    s_DICMDBUF[1], s_DICMDBUF[2]);
      SetDriveError(DriveError::InvalidAudioCommand);
      interrupt_type = DIInterruptType::DEINT;
      break;
    }
  }
  break;

  // Request Audio Status (Immediate). Only used by some GC games, but does exist on the Wii
  case DICommand::RequestAudioStatus:
  {
    if (!CheckReadPreconditions())
    {
      ERROR_LOG_FMT(DVDINTERFACE, "Attempted to request audio status in an invalid state!");
      interrupt_type = DIInterruptType::DEINT;
      break;
    }

    if (!s_enable_dtk)
    {
      ERROR_LOG_FMT(DVDINTERFACE, "Attempted to request audio status while audio is disabled!");
      SetDriveError(DriveError::NoAudioBuf);
      interrupt_type = DIInterruptType::DEINT;
      break;
    }

    switch (s_DICMDBUF[0] >> 16 & 0xFF)
    {
    case 0x00:  // Returns streaming status
      INFO_LOG_FMT(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);
      s_DIIMMBUF = (s_stream ? 1 : 0);
      break;
    case 0x01:  // Returns the current offset
      INFO_LOG_FMT(DVDINTERFACE, "(Audio): Stream Status: Request Audio status AudioPos:{:08x}",
                   s_audio_position);
      s_DIIMMBUF = static_cast<u32>((s_audio_position & 0xffffffffffff8000ull) >> 2);
      break;
    case 0x02:  // Returns the start offset
      INFO_LOG_FMT(DVDINTERFACE, "(Audio): Stream Status: Request Audio status CurrentStart:{:08x}",
                   s_current_start);
      s_DIIMMBUF = static_cast<u32>(s_current_start >> 2);
      break;
    case 0x03:  // Returns the total length
      INFO_LOG_FMT(DVDINTERFACE,
                   "(Audio): Stream Status: Request Audio status CurrentLength:{:08x}",
                   s_current_length);
      s_DIIMMBUF = s_current_length;
      break;
    default:
      ERROR_LOG_FMT(DVDINTERFACE, "Invalid audio status command!  ({:08x} {:08x} {:08x})",
                    s_DICMDBUF[0], s_DICMDBUF[1], s_DICMDBUF[2]);
      SetDriveError(DriveError::InvalidAudioCommand);
      interrupt_type = DIInterruptType::DEINT;
      break;
    }
  }
  break;

  // Used by both GC and Wii
  case DICommand::StopMotor:
  {
    const bool eject = (s_DICMDBUF[0] & (1 << 17));
    const bool kill = (s_DICMDBUF[0] & (1 << 20));
    INFO_LOG_FMT(DVDINTERFACE, "DVDLowStopMotor{}{}", eject ? " eject" : "", kill ? " kill!" : "");

    if (s_drive_state == DriveState::Ready || s_drive_state == DriveState::ReadyNoReadsMade ||
        s_drive_state == DriveState::DiscIdNotRead)
    {
      SetDriveState(DriveState::MotorStopped);
    }

    const bool force_eject = eject && !kill;

    if (Config::Get(Config::MAIN_AUTO_DISC_CHANGE) && !Movie::IsPlayingInput() &&
        DVDThread::IsInsertedDiscRunning() && !s_auto_disc_change_paths.empty())
    {
      CoreTiming::ScheduleEvent(force_eject ? 0 : SystemTimers::GetTicksPerSecond() / 2,
                                s_auto_change_disc);
      OSD::AddMessage("Changing discs automatically...", OSD::Duration::NORMAL);
    }
    else if (force_eject)
    {
      EjectDisc(EjectCause::Software);
    }
    break;
  }

  // DVD Audio Enable/Disable (Immediate). GC uses this, and the Wii can use it to configure GC
  // games.
  case DICommand::AudioBufferConfig:
    // The IPL uses this command to enable or disable DTK audio depending on the value of byte 0x8
    // in the disc header. See http://www.crazynation.org/GC/GC_DD_TECH/GCTech.htm for more info.
    // The link is dead, but you can access the page using the Wayback Machine at archive.org.

    // This command can only be used immediately after reading the disc ID, before any other
    // reads. Too early, and you get NoDiscID.  Too late, and you get InvalidPeriod.
    if (!CheckReadPreconditions())
    {
      ERROR_LOG_FMT(DVDINTERFACE, "Attempted to change DTK configuration in an invalid state!");
      interrupt_type = DIInterruptType::DEINT;
      break;
    }

    if (s_drive_state == DriveState::Ready)
    {
      ERROR_LOG_FMT(DVDINTERFACE,
                    "Attempted to change DTK configuration after a read has been made!");
      SetDriveError(DriveError::InvalidPeriod);
      interrupt_type = DIInterruptType::DEINT;
      break;
    }

    // Note that this can be called multiple times, as long as the drive is in the ReadyNoReadsMade
    // state. Calling it does not exit that state.
    AudioBufferConfig((s_DICMDBUF[0] >> 16) & 1, s_DICMDBUF[0] & 0xf);
    break;

  // GC-only patched drive firmware command, used by libogc
  case DICommand::UnknownEE:
    INFO_LOG_FMT(DVDINTERFACE, "SetStatus");
    SetDriveError(DriveError::InvalidCommand);
    interrupt_type = DIInterruptType::DEINT;
    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 only be used through direct access and only after unlocked.
  case DICommand::Debug:
    ERROR_LOG_FMT(DVDINTERFACE, "Unsupported DVD Drive debug command {:#010x}", s_DICMDBUF[0]);
    SetDriveError(DriveError::InvalidCommand);
    interrupt_type = DIInterruptType::DEINT;
    break;

  // Unlock Commands. 1: "MATSHITA" 2: "DVD-GAME"
  // Just for fun
  // Can only be used through direct access.  The unlock command doesn't seem to work on the Wii.
  case DICommand::DebugUnlock:
  {
    if (s_DICMDBUF[0] == 0xFF014D41 && s_DICMDBUF[1] == 0x54534849 && s_DICMDBUF[2] == 0x54410200)
    {
      INFO_LOG_FMT(DVDINTERFACE, "Unlock test 1 passed");
    }
    else if (s_DICMDBUF[0] == 0xFF004456 && s_DICMDBUF[1] == 0x442D4741 &&
             s_DICMDBUF[2] == 0x4D450300)
    {
      INFO_LOG_FMT(DVDINTERFACE, "Unlock test 2 passed");
    }
    else
    {
      INFO_LOG_FMT(DVDINTERFACE, "Unlock test failed");
    }
  }
  break;

  default:
    ERROR_LOG_FMT(DVDINTERFACE, "Unknown command {:#010x} (Buffer {:#010x}, {:#x})", s_DICMDBUF[0],
                  s_DIMAR, s_DILENGTH);
    PanicAlertFmtT("Unknown DVD command {0:08x} - fatal error", s_DICMDBUF[0]);
    SetDriveError(DriveError::InvalidCommand);
    interrupt_type = DIInterruptType::DEINT;
    break;
  }

  if (!command_handled_by_thread)
  {
    // TODO: Needs testing to determine if MINIMUM_COMMAND_LATENCY_US is accurate for this
    CoreTiming::ScheduleEvent(
        MINIMUM_COMMAND_LATENCY_US * (SystemTimers::GetTicksPerSecond() / 1000000),
        s_finish_executing_command, PackFinishExecutingCommandUserdata(reply_type, interrupt_type));
  }
}

void PerformDecryptingRead(u32 position, u32 length, u32 output_address,
                           const DiscIO::Partition& partition, ReplyType reply_type)
{
  DIInterruptType interrupt_type = DIInterruptType::TCINT;

  if (s_drive_state == DriveState::ReadyNoReadsMade)
    SetDriveState(DriveState::Ready);

  const bool command_handled_by_thread =
      ExecuteReadCommand(static_cast<u64>(position) << 2, output_address, length, length, partition,
                         reply_type, &interrupt_type);

  if (!command_handled_by_thread)
  {
    // TODO: Needs testing to determine if MINIMUM_COMMAND_LATENCY_US is accurate for this
    CoreTiming::ScheduleEvent(
        MINIMUM_COMMAND_LATENCY_US * (SystemTimers::GetTicksPerSecond() / 1000000),
        s_finish_executing_command, PackFinishExecutingCommandUserdata(reply_type, interrupt_type));
  }
}

void AudioBufferConfig(bool enable_dtk, u8 dtk_buffer_length)
{
  s_enable_dtk = enable_dtk;
  s_dtk_buffer_length = dtk_buffer_length;
  if (s_enable_dtk)
    INFO_LOG_FMT(DVDINTERFACE, "DTK enabled: buffer size {}", s_dtk_buffer_length);
  else
    INFO_LOG_FMT(DVDINTERFACE, "DTK disabled");
}

static u64 PackFinishExecutingCommandUserdata(ReplyType reply_type, DIInterruptType interrupt_type)
{
  return (static_cast<u64>(reply_type) << 32) + static_cast<u32>(interrupt_type);
}

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

void SetDriveState(DriveState state)
{
  s_drive_state = state;
}

void SetDriveError(DriveError error)
{
  s_error_code = error;
}

void FinishExecutingCommand(ReplyType reply_type, DIInterruptType interrupt_type, s64 cycles_late,
                            const std::vector<u8>& data)
{
  // The data parameter contains the requested data iff this was called from DVDThread, and is
  // empty otherwise. DVDThread is the only source of ReplyType::NoReply and ReplyType::DTK.

  u32 transfer_size = 0;
  if (reply_type == ReplyType::NoReply)
    transfer_size = static_cast<u32>(data.size());
  else if (reply_type == ReplyType::Interrupt || reply_type == ReplyType::IOS)
    transfer_size = s_DILENGTH;

  if (interrupt_type == DIInterruptType::TCINT)
  {
    s_DIMAR += transfer_size;
    s_DILENGTH -= transfer_size;
  }

  switch (reply_type)
  {
  case ReplyType::NoReply:
  {
    break;
  }

  case ReplyType::Interrupt:
  {
    if (s_DICR.TSTART)
    {
      s_DICR.TSTART = 0;
      GenerateDIInterrupt(interrupt_type);
    }
    break;
  }

  case ReplyType::IOS:
  {
    IOS::HLE::DIDevice::InterruptFromDVDInterface(interrupt_type);
    break;
  }

  case ReplyType::DTK:
  {
    DTKStreamingCallback(interrupt_type, data, cycles_late);
    break;
  }
  }
}

// Determines from a given read request how much of the request is buffered,
// and how much is required to be read from disc.
static void ScheduleReads(u64 offset, u32 length, const DiscIO::Partition& partition,
                          u32 output_address, ReplyType reply_type)
{
  // The drive continues to read 1 MiB beyond the last read position when idle.
  // If a future read falls within this window, part of the read may be returned
  // from the buffer. Data can be transferred from the buffer at up to 32 MiB/s.

  // Metroid Prime is a good example of a game that's sensitive to disc timing
  // details; if there isn't enough latency in the right places, doors can open
  // faster than on real hardware, and if there's too much latency in the wrong
  // places, the video before the save-file select screen lags.

  const u64 current_time = CoreTiming::GetTicks();
  const u32 ticks_per_second = SystemTimers::GetTicksPerSecond();
  const bool wii_disc = DVDThread::GetDiscType() == DiscIO::Platform::WiiDisc;

  // Whether we have performed a seek.
  bool seek = false;

  // Where the DVD read head is (usually parked at the end of the buffer,
  // unless we've interrupted it mid-buffer-read).
  u64 head_position;

  // Compute the start (inclusive) and end (exclusive) of the buffer.
  // If we fall within its bounds, we get DMA-speed reads.
  u64 buffer_start, buffer_end;

  // The variable offset uses the same addressing as games do.
  // The variable dvd_offset tracks the actual offset on the DVD
  // that the disc drive starts reading at, which differs in two ways:
  // It's rounded to a whole ECC block and never uses Wii partition addressing.
  u64 dvd_offset = DVDThread::PartitionOffsetToRawOffset(offset, partition);
  dvd_offset = Common::AlignDown(dvd_offset, DVD_ECC_BLOCK_SIZE);
  const u64 first_block = dvd_offset;

  if (Config::Get(Config::MAIN_FAST_DISC_SPEED))
  {
    // The SUDTR setting makes us act as if all reads are buffered
    buffer_start = std::numeric_limits<u64>::min();
    buffer_end = std::numeric_limits<u64>::max();
    head_position = 0;
  }
  else
  {
    if (s_read_buffer_start_time == s_read_buffer_end_time)
    {
      // No buffer
      buffer_start = buffer_end = head_position = 0;
    }
    else
    {
      buffer_start = s_read_buffer_end_offset > STREAMING_BUFFER_SIZE ?
                         s_read_buffer_end_offset - STREAMING_BUFFER_SIZE :
                         0;

      DEBUG_LOG_FMT(DVDINTERFACE, "Buffer: now={:#x} start time={:#x} end time={:#x}", current_time,
                    s_read_buffer_start_time, s_read_buffer_end_time);

      if (current_time >= s_read_buffer_end_time)
      {
        // Buffer is fully read
        buffer_end = s_read_buffer_end_offset;
      }
      else
      {
        // The amount of data the buffer contains *right now*, rounded to a DVD ECC block.
        buffer_end = s_read_buffer_start_offset +
                     Common::AlignDown((current_time - s_read_buffer_start_time) *
                                           (s_read_buffer_end_offset - s_read_buffer_start_offset) /
                                           (s_read_buffer_end_time - s_read_buffer_start_time),
                                       DVD_ECC_BLOCK_SIZE);
      }
      head_position = buffer_end;

      // Reading before the buffer is not only unbuffered,
      // but also destroys the old buffer for future reads.
      if (dvd_offset < buffer_start)
      {
        // Kill the buffer, but maintain the head position for seeks.
        buffer_start = buffer_end = 0;
      }
    }
  }

  DEBUG_LOG_FMT(DVDINTERFACE, "Buffer: start={:#x} end={:#x} avail={:#x}", buffer_start, buffer_end,
                buffer_end - buffer_start);

  DEBUG_LOG_FMT(DVDINTERFACE, "Schedule reads: offset={:#x} length={:#x} address={:#x}", offset,
                length, output_address);

  s64 ticks_until_completion =
      READ_COMMAND_LATENCY_US * (SystemTimers::GetTicksPerSecond() / 1000000);

  u32 buffered_blocks = 0;
  u32 unbuffered_blocks = 0;

  const u32 bytes_per_chunk =
      partition != DiscIO::PARTITION_NONE && DVDThread::IsEncryptedAndHashed() ?
          DiscIO::VolumeWii::BLOCK_DATA_SIZE :
          DVD_ECC_BLOCK_SIZE;

  do
  {
    // The length of this read - "+1" so that if this read is already
    // aligned to a block we'll read the entire block.
    u32 chunk_length = static_cast<u32>(Common::AlignUp(offset + 1, bytes_per_chunk) - offset);

    // The last chunk may be short
    chunk_length = std::min(chunk_length, length);

    // TODO: If the emulated software requests 0 bytes of data, should we seek or not?

    if (dvd_offset >= buffer_start && dvd_offset < buffer_end)
    {
      // Number of ticks it takes to transfer the data from the buffer to memory.
      // TODO: This calculation is slightly wrong when decrypt is true - it uses the size of
      // the copy from IOS to PPC but is supposed to model the copy from the disc drive to IOS.
      ticks_until_completion +=
          static_cast<u64>(chunk_length) * ticks_per_second / BUFFER_TRANSFER_RATE;
      buffered_blocks++;
    }
    else
    {
      // In practice we'll only ever seek if this is the first time
      // through this loop.
      if (dvd_offset != head_position)
      {
        // Unbuffered seek+read
        seek = true;
        ticks_until_completion += static_cast<u64>(
            ticks_per_second * DVDMath::CalculateSeekTime(head_position, dvd_offset));

        // TODO: The above emulates seeking and then reading one ECC block of data,
        // and then the below emulates the rotational latency. The rotational latency
        // should actually happen before reading data from the disc.

        const double time_after_seek =
            (CoreTiming::GetTicks() + ticks_until_completion) / ticks_per_second;
        ticks_until_completion += ticks_per_second * DVDMath::CalculateRotationalLatency(
                                                         dvd_offset, time_after_seek, wii_disc);

        DEBUG_LOG_FMT(DVDINTERFACE, "Seek+read {:#x} bytes @ {:#x} ticks={}", chunk_length, offset,
                      ticks_until_completion);
      }
      else
      {
        // Unbuffered read
        ticks_until_completion +=
            static_cast<u64>(ticks_per_second * DVDMath::CalculateRawDiscReadTime(
                                                    dvd_offset, DVD_ECC_BLOCK_SIZE, wii_disc));
      }

      unbuffered_blocks++;
      head_position = dvd_offset + DVD_ECC_BLOCK_SIZE;
    }

    // Schedule this read to complete at the appropriate time
    const ReplyType chunk_reply_type = chunk_length == length ? reply_type : ReplyType::NoReply;
    DVDThread::StartReadToEmulatedRAM(output_address, offset, chunk_length, partition,
                                      chunk_reply_type, ticks_until_completion);

    // Advance the read window
    output_address += chunk_length;
    offset += chunk_length;
    length -= chunk_length;
    dvd_offset += DVD_ECC_BLOCK_SIZE;
  } while (length > 0);

  // Evict blocks from the buffer which are unlikely to be used again after this read,
  // so that the buffer gets space for prefetching new blocks. Based on hardware testing,
  // the blocks which are kept are the most recently accessed block, the block immediately
  // before it, and all blocks after it.
  //
  // If the block immediately before the most recently accessed block is not kept, loading
  // screens in Pitfall: The Lost Expedition are longer than they should be.
  // https://bugs.dolphin-emu.org/issues/12279
  const u64 last_block = dvd_offset;
  constexpr u32 BUFFER_BACKWARD_SEEK_LIMIT = DVD_ECC_BLOCK_SIZE * 2;
  if (last_block - buffer_start <= BUFFER_BACKWARD_SEEK_LIMIT && buffer_start != buffer_end)
  {
    // Special case: reading the first two blocks of the buffer doesn't change the buffer state
  }
  else
  {
    // Note that the s_read_buffer_start_offset value we calculate here is not the
    // actual start of the buffer - it is just the start of the portion we need to read.
    // The actual start of the buffer is s_read_buffer_end_offset - STREAMING_BUFFER_SIZE.
    if (last_block >= buffer_end)
    {
      // Full buffer read
      s_read_buffer_start_offset = last_block;
    }
    else
    {
      // Partial buffer read
      s_read_buffer_start_offset = buffer_end;
    }

    s_read_buffer_end_offset = last_block + STREAMING_BUFFER_SIZE - BUFFER_BACKWARD_SEEK_LIMIT;
    if (seek)
    {
      // If we seek, the block preceding the first accessed block never gets read into the buffer
      s_read_buffer_end_offset =
          std::max(s_read_buffer_end_offset, first_block + STREAMING_BUFFER_SIZE);
    }

    // Assume the buffer starts prefetching new blocks right after the end of the last operation
    s_read_buffer_start_time = current_time + ticks_until_completion;
    s_read_buffer_end_time =
        s_read_buffer_start_time +
        static_cast<u64>(ticks_per_second *
                         DVDMath::CalculateRawDiscReadTime(
                             s_read_buffer_start_offset,
                             s_read_buffer_end_offset - s_read_buffer_start_offset, wii_disc));
  }

  DEBUG_LOG_FMT(DVDINTERFACE,
                "Schedule reads: ECC blocks unbuffered={}, buffered={}, "
                "ticks={}, time={} us",
                unbuffered_blocks, buffered_blocks, ticks_until_completion,
                ticks_until_completion * 1000000 / SystemTimers::GetTicksPerSecond());
}

}  // namespace DVDInterface
