/**
 ******************************************************************************
 * Xenia : Xbox 360 Emulator Research Project                                 *
 ******************************************************************************
 * Copyright 2013 Ben Vanik. All rights reserved.                             *
 * Released under the BSD license - see LICENSE in the root for more details. *
 ******************************************************************************
 */

#include "xenia/apu/xaudio2/xaudio2_audio_driver.h"

// Must be included before xaudio2.h so we get the right windows.h include.
#include "xenia/base/platform_win.h"

#include <xaudio2.h>  // NOLINT(build/include_order)

#include "xenia/apu/apu_flags.h"
#include "xenia/base/clock.h"
#include "xenia/base/logging.h"

namespace xe {
namespace apu {
namespace xaudio2 {

class XAudio2AudioDriver::VoiceCallback : public IXAudio2VoiceCallback {
 public:
  explicit VoiceCallback(xe::threading::Semaphore* semaphore)
      : semaphore_(semaphore) {}
  ~VoiceCallback() {}

  void OnStreamEnd() {}
  void OnVoiceProcessingPassEnd() {}
  void OnVoiceProcessingPassStart(uint32_t samples_required) {}
  void OnBufferEnd(void* context) {
    auto ret = semaphore_->Release(1, nullptr);
    assert_true(ret);
  }
  void OnBufferStart(void* context) {}
  void OnLoopEnd(void* context) {}
  void OnVoiceError(void* context, HRESULT result) {}

 private:
  xe::threading::Semaphore* semaphore_ = nullptr;
};

XAudio2AudioDriver::XAudio2AudioDriver(Memory* memory,
                                       xe::threading::Semaphore* semaphore)
    : AudioDriver(memory), semaphore_(semaphore) {
  static_assert(frame_count_ == XAUDIO2_MAX_QUEUED_BUFFERS,
                "xaudio header differs");
}

XAudio2AudioDriver::~XAudio2AudioDriver() = default;

const DWORD ChannelMasks[] = {
    0,
    0,
    SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_LOW_FREQUENCY,
    0,
    0,
    0,
    SPEAKER_FRONT_LEFT | SPEAKER_FRONT_CENTER | SPEAKER_FRONT_RIGHT |
        SPEAKER_LOW_FREQUENCY | SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT,
    0,
};

bool XAudio2AudioDriver::Initialize() {
  HRESULT hr;

  voice_callback_ = new VoiceCallback(semaphore_);

  // Load XAudio2_8.dll dynamically - Windows 8.1 SDK references XAudio2_8.dll
  // in xaudio2.lib, which is available on Windows 8.1, however, Windows 10 SDK
  // references XAudio2_9.dll in it, which is only available in Windows 10, and
  // XAudio2_8.dll is linked through a different .lib - xaudio2_8.lib, so easier
  // not to link the .lib at all.
  xaudio2_module_ = reinterpret_cast<void*>(LoadLibrary(L"XAudio2_8.dll"));
  if (!xaudio2_module_) {
    XELOGE("LoadLibrary(XAudio2_8.dll) failed");
    assert_always();
    return false;
  }

  union {
    HRESULT(__stdcall* xaudio2_create)
    (IXAudio2** xaudio2_out, UINT32 flags, XAUDIO2_PROCESSOR xaudio2_processor);
    FARPROC xaudio2_create_ptr;
  };
  xaudio2_create_ptr = GetProcAddress(
      reinterpret_cast<HMODULE>(xaudio2_module_), "XAudio2Create");
  if (!xaudio2_create_ptr) {
    XELOGE("GetProcAddress(XAudio2_8.dll, XAudio2Create) failed");
    assert_always();
    return false;
  }

  hr = xaudio2_create(&audio_, 0, XAUDIO2_DEFAULT_PROCESSOR);
  if (FAILED(hr)) {
    XELOGE("XAudio2Create failed with %.8X", hr);
    assert_always();
    return false;
  }

  XAUDIO2_DEBUG_CONFIGURATION config;
  config.TraceMask = XAUDIO2_LOG_ERRORS | XAUDIO2_LOG_WARNINGS;
  config.BreakMask = 0;
  config.LogThreadID = FALSE;
  config.LogTiming = TRUE;
  config.LogFunctionName = TRUE;
  config.LogFileline = TRUE;
  audio_->SetDebugConfiguration(&config);

  hr = audio_->CreateMasteringVoice(&mastering_voice_);
  if (FAILED(hr)) {
    XELOGE("CreateMasteringVoice failed with %.8X", hr);
    assert_always();
    return false;
  }

  WAVEFORMATIEEEFLOATEX waveformat;

  waveformat.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
  waveformat.Format.nChannels = frame_channels_;
  waveformat.Format.nSamplesPerSec = 48000;
  waveformat.Format.wBitsPerSample = 32;
  waveformat.Format.nBlockAlign =
      (waveformat.Format.nChannels * waveformat.Format.wBitsPerSample) / 8;
  waveformat.Format.nAvgBytesPerSec =
      waveformat.Format.nSamplesPerSec * waveformat.Format.nBlockAlign;
  waveformat.Format.cbSize =
      sizeof(WAVEFORMATIEEEFLOATEX) - sizeof(WAVEFORMATEX);

  waveformat.SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
  waveformat.Samples.wValidBitsPerSample = waveformat.Format.wBitsPerSample;
  waveformat.dwChannelMask = ChannelMasks[waveformat.Format.nChannels];

  hr = audio_->CreateSourceVoice(
      &pcm_voice_, &waveformat.Format,
      0,  // XAUDIO2_VOICE_NOSRC | XAUDIO2_VOICE_NOPITCH,
      XAUDIO2_MAX_FREQ_RATIO, voice_callback_);
  if (FAILED(hr)) {
    XELOGE("CreateSourceVoice failed with %.8X", hr);
    assert_always();
    return false;
  }

  hr = pcm_voice_->Start();
  if (FAILED(hr)) {
    XELOGE("Start failed with %.8X", hr);
    assert_always();
    return false;
  }

  if (cvars::mute) {
    pcm_voice_->SetVolume(0.0f);
  }

  return true;
}

void XAudio2AudioDriver::SubmitFrame(uint32_t frame_ptr) {
  // Process samples! They are big-endian floats.
  HRESULT hr;

  XAUDIO2_VOICE_STATE state;
  pcm_voice_->GetState(&state, XAUDIO2_VOICE_NOSAMPLESPLAYED);
  assert_true(state.BuffersQueued < frame_count_);

  auto input_frame = memory_->TranslateVirtual<float*>(frame_ptr);
  auto output_frame = reinterpret_cast<float*>(frames_[current_frame_]);
  auto interleave_channels = frame_channels_;

  // interleave the data
  for (uint32_t index = 0, o = 0; index < channel_samples_; ++index) {
    for (uint32_t channel = 0, table = 0; channel < interleave_channels;
         ++channel, table += channel_samples_) {
      output_frame[o++] = xe::byte_swap(input_frame[table + index]);
    }
  }

  XAUDIO2_BUFFER buffer;
  buffer.Flags = 0;
  buffer.pAudioData = reinterpret_cast<BYTE*>(output_frame);
  buffer.AudioBytes = frame_size_;
  buffer.PlayBegin = 0;
  buffer.PlayLength = channel_samples_;
  buffer.LoopBegin = XAUDIO2_NO_LOOP_REGION;
  buffer.LoopLength = 0;
  buffer.LoopCount = 0;
  buffer.pContext = 0;
  hr = pcm_voice_->SubmitSourceBuffer(&buffer);
  if (FAILED(hr)) {
    XELOGE("SubmitSourceBuffer failed with %.8X", hr);
    assert_always();
    return;
  }

  current_frame_ = (current_frame_ + 1) % frame_count_;

  // Update playback ratio to our time scalar.
  // This will keep audio in sync with the game clock.
  pcm_voice_->SetFrequencyRatio(
      static_cast<float>(xe::Clock::guest_time_scalar()));
}

void XAudio2AudioDriver::Shutdown() {
  if (pcm_voice_) {
    pcm_voice_->Stop();
    pcm_voice_->DestroyVoice();
    pcm_voice_ = NULL;
  }

  if (mastering_voice_) {
    mastering_voice_->DestroyVoice();
    mastering_voice_ = NULL;
  }

  if (audio_) {
    audio_->StopEngine();
    audio_->Release();
    audio_ = nullptr;
  }

  if (xaudio2_module_) {
    FreeLibrary(reinterpret_cast<HMODULE>(xaudio2_module_));
    xaudio2_module_ = nullptr;
  }

  if (voice_callback_) {
    delete voice_callback_;
    voice_callback_ = nullptr;
  }
}

}  // namespace xaudio2
}  // namespace apu
}  // namespace xe
