/*
 *  Copyright (C) 2011-2018 Team Kodi
 *  This file is part of Kodi - https://kodi.tv
 *
 *  SPDX-License-Identifier: GPL-2.0-or-later
 *  See LICENSES/README.md for more information.
 */

#include "CoreAudioStream.h"

#include "CoreAudioDevice.h"
#include "cores/AudioEngine/Sinks/darwin/CoreAudioHelpers.h"
#include "utils/log.h"

using namespace std::chrono_literals;

CCoreAudioStream::CCoreAudioStream()
{
  m_OriginalVirtualFormat.mFormatID = 0;
  m_OriginalPhysicalFormat.mFormatID = 0;
}

CCoreAudioStream::~CCoreAudioStream()
{
  Close();
}

bool CCoreAudioStream::Open(AudioStreamID streamId)
{
  m_StreamId = streamId;
  CLog::Log(LOGDEBUG, "CCoreAudioStream::Open: Opened stream {:#04x}.", (uint)m_StreamId);

  // watch for physical property changes.
  AudioObjectPropertyAddress propertyAOPA;
  propertyAOPA.mScope    = kAudioObjectPropertyScopeGlobal;
  propertyAOPA.mElement  = kAudioObjectPropertyElementMaster;
  propertyAOPA.mSelector = kAudioStreamPropertyPhysicalFormat;
  if (AudioObjectAddPropertyListener(m_StreamId, &propertyAOPA, HardwareStreamListener, this) != noErr)
    CLog::Log(LOGERROR, "CCoreAudioStream::Open: couldn't set up a physical property listener.");

  // watch for virtual property changes.
  propertyAOPA.mScope    = kAudioObjectPropertyScopeGlobal;
  propertyAOPA.mElement  = kAudioObjectPropertyElementMaster;
  propertyAOPA.mSelector = kAudioStreamPropertyVirtualFormat;
  if (AudioObjectAddPropertyListener(m_StreamId, &propertyAOPA, HardwareStreamListener, this) != noErr)
    CLog::Log(LOGERROR, "CCoreAudioStream::Open: couldn't set up a virtual property listener.");

  return true;
}

//! @todo Should it even be possible to change both the
//! physical and virtual formats, since the devices do it themselves?
void CCoreAudioStream::Close(bool restore)
{
  if (!m_StreamId)
    return;

  std::string formatString;

  // remove the physical/virtual property listeners before we make changes
  // that will trigger callbacks that we do not care about.
  AudioObjectPropertyAddress propertyAOPA;
  propertyAOPA.mScope    = kAudioObjectPropertyScopeGlobal;
  propertyAOPA.mElement  = kAudioObjectPropertyElementMaster;
  propertyAOPA.mSelector = kAudioStreamPropertyPhysicalFormat;
  if (AudioObjectRemovePropertyListener(m_StreamId, &propertyAOPA, HardwareStreamListener, this) != noErr)
    CLog::Log(LOGDEBUG, "CCoreAudioStream::Close: Couldn't remove property listener.");

  propertyAOPA.mScope    = kAudioObjectPropertyScopeGlobal;
  propertyAOPA.mElement  = kAudioObjectPropertyElementMaster;
  propertyAOPA.mSelector = kAudioStreamPropertyVirtualFormat;
  if (AudioObjectRemovePropertyListener(m_StreamId, &propertyAOPA, HardwareStreamListener, this) != noErr)
    CLog::Log(LOGDEBUG, "CCoreAudioStream::Close: Couldn't remove property listener.");

  // Revert any format changes we made
  if (restore && m_OriginalVirtualFormat.mFormatID && m_StreamId)
  {
    CLog::Log(LOGDEBUG,
              "CCoreAudioStream::Close: "
              "Restoring original virtual format for stream {:#04x}. ({})",
              (uint)m_StreamId, StreamDescriptionToString(m_OriginalVirtualFormat, formatString));
    AudioStreamBasicDescription setFormat = m_OriginalVirtualFormat;
    SetVirtualFormat(&setFormat);
  }
  if (restore && m_OriginalPhysicalFormat.mFormatID && m_StreamId)
  {
    CLog::Log(LOGDEBUG,
              "CCoreAudioStream::Close: "
              "Restoring original physical format for stream {:#04x}. ({})",
              (uint)m_StreamId, StreamDescriptionToString(m_OriginalPhysicalFormat, formatString));
    AudioStreamBasicDescription setFormat = m_OriginalPhysicalFormat;
    SetPhysicalFormat(&setFormat);
  }

  m_OriginalVirtualFormat.mFormatID  = 0;
  m_OriginalPhysicalFormat.mFormatID = 0;
  CLog::Log(LOGDEBUG, "CCoreAudioStream::Close: Closed stream {:#04x}.", (uint)m_StreamId);
  m_StreamId = 0;
}

UInt32 CCoreAudioStream::GetDirection()
{
  if (!m_StreamId)
    return 0;

  UInt32 val = 0;
  UInt32 size = sizeof(UInt32);

  AudioObjectPropertyAddress propertyAddress;
  propertyAddress.mScope    = kAudioObjectPropertyScopeGlobal;
  propertyAddress.mElement  = kAudioObjectPropertyElementMaster;
  propertyAddress.mSelector = kAudioStreamPropertyDirection;

  OSStatus ret = AudioObjectGetPropertyData(m_StreamId, &propertyAddress, 0, NULL, &size, &val);
  if (ret)
    return 0;

  return val;
}

// WARNING - don't rely on this method - the return value of
// GetTerminalType is driver specific - the checked return
// values are only recommendations from apple
bool CCoreAudioStream::IsDigitalOutput(AudioStreamID id)
{
  UInt32 type = GetTerminalType(id);
  // yes apple is mixing types here...
  return (type == kAudioStreamTerminalTypeDigitalAudioInterface ||
          type == kIOAudioDeviceTransportTypeDisplayPort ||
          type == kIOAudioDeviceTransportTypeHdmi ||
          type == kIOAudioDeviceTransportTypeFireWire ||
          type == kIOAudioDeviceTransportTypeThunderbolt ||
          type == kIOAudioDeviceTransportTypeUSB);
}

bool CCoreAudioStream::GetStartingChannelInDevice(AudioStreamID id, UInt32 &startingChannel)
{
  if (!id)
    return false;

  UInt32 i_param_size = sizeof(UInt32);
  UInt32 i_param;
  startingChannel = 0;
  bool ret = false;

  AudioObjectPropertyAddress propertyAddress;
  propertyAddress.mScope    = kAudioObjectPropertyScopeGlobal;
  propertyAddress.mElement  = kAudioObjectPropertyElementMaster;
  propertyAddress.mSelector = kAudioStreamPropertyStartingChannel;

  // number of frames of latency in the AudioStream
  OSStatus status = AudioObjectGetPropertyData(id, &propertyAddress, 0, NULL, &i_param_size, &i_param);
  if (status == noErr)
  {
    startingChannel = i_param;
    ret = true;
  }

  return ret;
}

UInt32 CCoreAudioStream::GetTerminalType(AudioStreamID id)
{
  if (!id)
    return 0;

  UInt32 val = 0;
  UInt32 size = sizeof(UInt32);

  AudioObjectPropertyAddress propertyAddress;
  propertyAddress.mScope    = kAudioObjectPropertyScopeGlobal;
  propertyAddress.mElement  = kAudioObjectPropertyElementMaster;
  propertyAddress.mSelector = kAudioStreamPropertyTerminalType;

  OSStatus ret = AudioObjectGetPropertyData(id, &propertyAddress, 0, NULL, &size, &val);
  if (ret)
    return 0;
  return val;
}

UInt32 CCoreAudioStream::GetNumLatencyFrames()
{
  if (!m_StreamId)
    return 0;

  UInt32 i_param_size = sizeof(uint32_t);
  UInt32 i_param, num_latency_frames = 0;

  AudioObjectPropertyAddress propertyAddress;
  propertyAddress.mScope    = kAudioObjectPropertyScopeGlobal;
  propertyAddress.mElement  = kAudioObjectPropertyElementMaster;
  propertyAddress.mSelector = kAudioStreamPropertyLatency;

  // number of frames of latency in the AudioStream
  OSStatus ret = AudioObjectGetPropertyData(m_StreamId, &propertyAddress, 0, NULL, &i_param_size, &i_param);
  if (ret == noErr)
  {
    num_latency_frames += i_param;
  }

  return num_latency_frames;
}

bool CCoreAudioStream::GetVirtualFormat(AudioStreamBasicDescription* pDesc)
{
  if (!pDesc || !m_StreamId)
    return false;

  UInt32 size = sizeof(AudioStreamBasicDescription);

  AudioObjectPropertyAddress propertyAddress;
  propertyAddress.mScope    = kAudioObjectPropertyScopeGlobal;
  propertyAddress.mElement  = kAudioObjectPropertyElementMaster;
  propertyAddress.mSelector = kAudioStreamPropertyVirtualFormat;
  OSStatus ret = AudioObjectGetPropertyDataSize(m_StreamId, &propertyAddress, 0, NULL, &size);
  if (ret)
    return false;

  ret = AudioObjectGetPropertyData(m_StreamId, &propertyAddress, 0, NULL, &size, pDesc);
  if (ret)
    return false;
  return true;
}

bool CCoreAudioStream::SetVirtualFormat(AudioStreamBasicDescription* pDesc)
{
  if (!pDesc || !m_StreamId)
    return false;

  std::string formatString;

  // suppress callbacks for the default output device change
  // for the next 2 seconds because setting format
  // might trigger a change (when setting/unsetting an encoded
  // passthrough format)
  CCoreAudioDevice::SuppressDefaultOutputDeviceCB(2000);


  if (!m_OriginalVirtualFormat.mFormatID)
  {
    // Store the original format (as we found it) so that it can be restored later
    if (!GetVirtualFormat(&m_OriginalVirtualFormat))
    {
      CLog::Log(LOGERROR,
                "CCoreAudioStream::SetVirtualFormat: "
                "Unable to retrieve current virtual format for stream {:#04x}.",
                (uint)m_StreamId);
      return false;
    }
  }
  m_virtual_format_event.Reset();

  AudioObjectPropertyAddress propertyAddress;
  propertyAddress.mScope    = kAudioObjectPropertyScopeGlobal;
  propertyAddress.mElement  = kAudioObjectPropertyElementMaster;
  propertyAddress.mSelector = kAudioStreamPropertyVirtualFormat;

  UInt32 propertySize = sizeof(AudioStreamBasicDescription);
  OSStatus ret = AudioObjectSetPropertyData(m_StreamId, &propertyAddress, 0, NULL, propertySize, pDesc);
  if (ret)
  {
    CLog::Log(LOGERROR,
              "CCoreAudioStream::SetVirtualFormat: "
              "Unable to set virtual format for stream {:#04x}. Error = {}",
              (uint)m_StreamId, GetError(ret));
    return false;
  }

  // The AudioStreamSetProperty is not only asynchronous,
  // it is also not Atomic, in its behaviour.
  // Therefore we check 5 times before we really give up.
  // FIXME: failing isn't actually implemented yet.
  for (int i = 0; i < 10; ++i)
  {
    AudioStreamBasicDescription checkVirtualFormat;
    if (!GetVirtualFormat(&checkVirtualFormat))
    {
      CLog::Log(LOGERROR,
                "CCoreAudioStream::SetVirtualFormat: "
                "Unable to retrieve current physical format for stream {:#04x}.",
                (uint)m_StreamId);
      return false;
    }
    if (checkVirtualFormat.mSampleRate == pDesc->mSampleRate &&
        checkVirtualFormat.mFormatID == pDesc->mFormatID &&
        checkVirtualFormat.mFramesPerPacket == pDesc->mFramesPerPacket)
    {
      // The right format is now active.
      CLog::Log(LOGDEBUG,
                "CCoreAudioStream::SetVirtualFormat: "
                "Virtual format for stream {:#04x}. now active ({})",
                (uint)m_StreamId, StreamDescriptionToString(checkVirtualFormat, formatString));
      break;
    }
    m_virtual_format_event.Wait(100ms);
  }
  return true;
}

bool CCoreAudioStream::GetPhysicalFormat(AudioStreamBasicDescription* pDesc)
{
  if (!pDesc || !m_StreamId)
    return false;

  UInt32 size = sizeof(AudioStreamBasicDescription);

  AudioObjectPropertyAddress propertyAddress;
  propertyAddress.mScope    = kAudioObjectPropertyScopeGlobal;
  propertyAddress.mElement  = kAudioObjectPropertyElementMaster;
  propertyAddress.mSelector = kAudioStreamPropertyPhysicalFormat;

  OSStatus ret = AudioObjectGetPropertyData(m_StreamId, &propertyAddress, 0, NULL, &size, pDesc);
  if (ret)
    return false;
  return true;
}

bool CCoreAudioStream::SetPhysicalFormat(AudioStreamBasicDescription* pDesc)
{
  if (!pDesc || !m_StreamId)
    return false;

  std::string formatString;

  // suppress callbacks for the default output device change
  // for the next 2 seconds because setting format
  // might trigger a change (when setting/unsetting an encoded
  // passthrough format)
  CCoreAudioDevice::SuppressDefaultOutputDeviceCB(2000);

  if (!m_OriginalPhysicalFormat.mFormatID)
  {
    // Store the original format (as we found it) so that it can be restored later
    if (!GetPhysicalFormat(&m_OriginalPhysicalFormat))
    {
      CLog::Log(LOGERROR,
                "CCoreAudioStream::SetPhysicalFormat: "
                "Unable to retrieve current physical format for stream {:#04x}.",
                (uint)m_StreamId);
      return false;
    }
  }
  m_physical_format_event.Reset();

  AudioObjectPropertyAddress propertyAddress;
  propertyAddress.mScope    = kAudioObjectPropertyScopeGlobal;
  propertyAddress.mElement  = kAudioObjectPropertyElementMaster;
  propertyAddress.mSelector = kAudioStreamPropertyPhysicalFormat;

  UInt32 propertySize = sizeof(AudioStreamBasicDescription);
  OSStatus ret = AudioObjectSetPropertyData(m_StreamId, &propertyAddress, 0, NULL, propertySize, pDesc);
  if (ret)
  {
    CLog::Log(LOGERROR,
              "CCoreAudioStream::SetPhysicalFormat: "
              "Unable to set physical format for stream {:#04x}. Error = {}",
              (uint)m_StreamId, GetError(ret));
    return false;
  }

  // The AudioStreamSetProperty is not only asynchronous,
  // it is also not Atomic, in its behaviour.
  // Therefore we check 5 times before we really give up.
  // FIXME: failing isn't actually implemented yet.
  for(int i = 0; i < 10; ++i)
  {
    AudioStreamBasicDescription checkPhysicalFormat;
    if (!GetPhysicalFormat(&checkPhysicalFormat))
    {
      CLog::Log(LOGERROR,
                "CCoreAudioStream::SetPhysicalFormat: "
                "Unable to retrieve current physical format for stream {:#04x}.",
                (uint)m_StreamId);
      return false;
    }
    if (checkPhysicalFormat.mSampleRate == pDesc->mSampleRate &&
        checkPhysicalFormat.mFormatID   == pDesc->mFormatID   &&
        checkPhysicalFormat.mFramesPerPacket == pDesc->mFramesPerPacket &&
        checkPhysicalFormat.mChannelsPerFrame == pDesc->mChannelsPerFrame)
    {
      // The right format is now active.
      CLog::Log(LOGDEBUG,
                "CCoreAudioStream::SetPhysicalFormat: "
                "Physical format for stream {:#04x}. now active ({})",
                (uint)m_StreamId, StreamDescriptionToString(checkPhysicalFormat, formatString));
      break;
    }
    m_physical_format_event.Wait(100ms);
  }

  return true;
}

bool CCoreAudioStream::GetAvailableVirtualFormats(StreamFormatList* pList)
{
  return GetAvailableVirtualFormats(m_StreamId, pList);
}

bool CCoreAudioStream::GetAvailableVirtualFormats(AudioStreamID id, StreamFormatList* pList)
{
  if (!pList || !id)
    return false;

  AudioObjectPropertyAddress propertyAddress;
  propertyAddress.mScope    = kAudioObjectPropertyScopeGlobal;
  propertyAddress.mElement  = kAudioObjectPropertyElementMaster;
  propertyAddress.mSelector = kAudioStreamPropertyAvailableVirtualFormats;

  UInt32 propertySize = 0;
  OSStatus ret = AudioObjectGetPropertyDataSize(id, &propertyAddress, 0, NULL, &propertySize);
  if (ret)
    return false;

  UInt32 formatCount = propertySize / sizeof(AudioStreamRangedDescription);
  AudioStreamRangedDescription *pFormatList = new AudioStreamRangedDescription[formatCount];
  ret = AudioObjectGetPropertyData(id, &propertyAddress, 0, NULL, &propertySize, pFormatList);
  if (!ret)
  {
    for (UInt32 format = 0; format < formatCount; format++)
      pList->push_back(pFormatList[format]);
  }
  delete[] pFormatList;
  return (ret == noErr);
}

bool CCoreAudioStream::GetAvailablePhysicalFormats(StreamFormatList* pList)
{
  return GetAvailablePhysicalFormats(m_StreamId, pList);
}

bool CCoreAudioStream::GetAvailablePhysicalFormats(AudioStreamID id, StreamFormatList* pList)
{
  if (!pList || !id)
    return false;

  AudioObjectPropertyAddress propertyAddress;
  propertyAddress.mScope    = kAudioObjectPropertyScopeGlobal;
  propertyAddress.mElement  = kAudioObjectPropertyElementMaster;
  propertyAddress.mSelector = kAudioStreamPropertyAvailablePhysicalFormats;

  UInt32 propertySize = 0;
  OSStatus ret = AudioObjectGetPropertyDataSize(id, &propertyAddress, 0, NULL, &propertySize);
  if (ret)
    return false;

  UInt32 formatCount = propertySize / sizeof(AudioStreamRangedDescription);
  AudioStreamRangedDescription *pFormatList = new AudioStreamRangedDescription[formatCount];
  ret = AudioObjectGetPropertyData(id, &propertyAddress, 0, NULL, &propertySize, pFormatList);
  if (!ret)
  {
    for (UInt32 format = 0; format < formatCount; format++)
      pList->push_back(pFormatList[format]);
  }
  delete[] pFormatList;
  return (ret == noErr);
}

OSStatus CCoreAudioStream::HardwareStreamListener(AudioObjectID inObjectID,
  UInt32 inNumberAddresses, const AudioObjectPropertyAddress inAddresses[], void *inClientData)
{
  CCoreAudioStream *ca_stream = (CCoreAudioStream*)inClientData;

  for (UInt32 i = 0; i < inNumberAddresses; i++)
  {
    if (inAddresses[i].mSelector == kAudioStreamPropertyPhysicalFormat)
    {
      AudioStreamBasicDescription actualFormat;
      UInt32 propertySize = sizeof(AudioStreamBasicDescription);
      // hardware physical format has changed.
      if (AudioObjectGetPropertyData(ca_stream->m_StreamId, &inAddresses[i], 0, NULL, &propertySize, &actualFormat) == noErr)
      {
        std::string formatString;
        CLog::Log(LOGINFO,
                  "CCoreAudioStream::HardwareStreamListener: "
                  "Hardware physical format changed to {}",
                  StreamDescriptionToString(actualFormat, formatString));
        ca_stream->m_physical_format_event.Set();
      }
    }
    else if (inAddresses[i].mSelector == kAudioStreamPropertyVirtualFormat)
    {
      // hardware virtual format has changed.
      AudioStreamBasicDescription actualFormat;
      UInt32 propertySize = sizeof(AudioStreamBasicDescription);
      if (AudioObjectGetPropertyData(ca_stream->m_StreamId, &inAddresses[i], 0, NULL, &propertySize, &actualFormat) == noErr)
      {
        std::string formatString;
        CLog::Log(LOGINFO,
                  "CCoreAudioStream::HardwareStreamListener: "
                  "Hardware virtual format changed to {}",
                  StreamDescriptionToString(actualFormat, formatString));
        ca_stream->m_virtual_format_event.Set();
      }
    }
  }

  return noErr;
}
