/*
 * GStreamer
 * Copyright (C) 2012-2013 Fluendo S.A. <support@fluendo.com>
 *   Authors: Josep Torra Vallès <josep@fluendo.com>
 *            Andoni Morales Alastruey <amorales@fluendo.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 *
 */

#include "gstosxcoreaudiocommon.h"

GST_DEBUG_CATEGORY_EXTERN (osx_coreaudio_debug);
#define GST_CAT_DEFAULT osx_coreaudio_debug

void
gst_core_audio_remove_render_callback (GstCoreAudio * core_audio)
{
  AURenderCallbackStruct input;
  OSStatus status;
  AudioUnitPropertyID callback_type;

  /* Deactivate the render callback by calling SetRenderCallback
   * with a NULL inputProc.
   */
  input.inputProc = NULL;
  input.inputProcRefCon = NULL;
  callback_type = core_audio->is_src ?
      kAudioOutputUnitProperty_SetInputCallback :
      kAudioUnitProperty_SetRenderCallback;

  status = AudioUnitSetProperty (core_audio->audiounit, callback_type, kAudioUnitScope_Global, 0,       /* N/A for global */
      &input, sizeof (input));

  if (status) {
    GST_WARNING_OBJECT (core_audio->osxbuf,
        "Failed to remove render callback %d", (int) status);
  }

  /* We're deactivated.. */
  g_atomic_int_set (&core_audio->io_proc_dropping, FALSE);
  core_audio->io_proc_active = FALSE;
}

gboolean
gst_core_audio_io_proc_start (GstCoreAudio * core_audio)
{
  OSStatus status;
  AURenderCallbackStruct input;
  AudioUnitPropertyID callback_type;

  GST_DEBUG_OBJECT (core_audio->osxbuf,
      "osx ring buffer start ioproc: %p device_id %lu",
      core_audio->element->io_proc, (gulong) core_audio->device_id);
  if (!core_audio->io_proc_active) {
    callback_type = core_audio->is_src ?
        kAudioOutputUnitProperty_SetInputCallback :
        kAudioUnitProperty_SetRenderCallback;

    input.inputProc = (AURenderCallback) core_audio->element->io_proc;
    input.inputProcRefCon = core_audio->osxbuf;

    status = AudioUnitSetProperty (core_audio->audiounit, callback_type, kAudioUnitScope_Global, 0,     /* N/A for global */
        &input, sizeof (input));

    if (status) {
      GST_ERROR_OBJECT (core_audio->osxbuf,
          "AudioUnitSetProperty failed: %d", (int) status);
      return FALSE;
    }

    core_audio->io_proc_active = TRUE;
  }

  g_atomic_int_set (&core_audio->io_proc_dropping, FALSE);

  // AudioOutputUnitStart on iOS can wait for the render callback to finish,
  // where in our case we set the ringbuffer timestamp, which also needs the ringbuf lock.
  GST_OBJECT_UNLOCK (core_audio->osxbuf);
  status = AudioOutputUnitStart (core_audio->audiounit);
  GST_OBJECT_LOCK (core_audio->osxbuf);
  if (status) {
    GST_ERROR_OBJECT (core_audio->osxbuf, "AudioOutputUnitStart failed: %d",
        (int) status);
    return FALSE;
  }
  return TRUE;
}

gboolean
gst_core_audio_io_proc_stop (GstCoreAudio * core_audio)
{
  OSErr status;

  GST_DEBUG_OBJECT (core_audio->osxbuf,
      "osx ring buffer stop ioproc: %p device_id %lu",
      core_audio->element->io_proc, (gulong) core_audio->device_id);

  status = AudioOutputUnitStop (core_audio->audiounit);
  if (status) {
    GST_WARNING_OBJECT (core_audio->osxbuf,
        "AudioOutputUnitStop failed: %d", (int) status);
  }
  // Ok to remove directly here because we stopped the AudioUnit already
  if (core_audio->io_proc_active) {
    gst_core_audio_remove_render_callback (core_audio);
  }
  return TRUE;
}

AudioBufferList *
buffer_list_alloc (UInt32 channels, UInt32 size, gboolean interleaved)
{
  AudioBufferList *list;
  gsize list_size;
  UInt32 num_buffers, n;

  num_buffers = interleaved ? 1 : channels;
  /* AudioBufferList member mBuffers is variable-length array */
  list_size = G_STRUCT_OFFSET (AudioBufferList, mBuffers[num_buffers]);
  list = (AudioBufferList *) g_malloc (list_size);

  list->mNumberBuffers = num_buffers;
  for (n = 0; n < num_buffers; ++n) {
    /* See http://lists.apple.com/archives/coreaudio-api/2015/Feb/msg00027.html */
    list->mBuffers[n].mNumberChannels = interleaved ? channels : 1;
    /* AudioUnitRender will keep overwriting mDataByteSize */
    list->mBuffers[n].mDataByteSize = size;
    list->mBuffers[n].mData = g_malloc (size);
  }

  return list;
}

void
buffer_list_free (AudioBufferList * list)
{
  UInt32 n;

  if (list == NULL)
    return;

  for (n = 0; n < list->mNumberBuffers; ++n) {
    g_free (list->mBuffers[n].mData);
  }

  g_free (list);
}

gboolean
gst_core_audio_bind_device (GstCoreAudio * core_audio)
{
  OSStatus status;

  /* Specify which device we're using. */
  GST_DEBUG_OBJECT (core_audio->osxbuf, "Bind AudioUnit to device %s",
      core_audio->unique_id);
  status = AudioUnitSetProperty (core_audio->audiounit,
      kAudioOutputUnitProperty_CurrentDevice, kAudioUnitScope_Global, 0,
      &core_audio->device_id, sizeof (AudioDeviceID));
  if (status) {
    GST_ERROR_OBJECT (core_audio->osxbuf, "Failed binding to device: %d",
        (int) status);
    goto audiounit_error;
  }
  return TRUE;

audiounit_error:
  if (core_audio->recBufferList) {
    buffer_list_free (core_audio->recBufferList);
    core_audio->recBufferList = NULL;
  }
  return FALSE;
}

static gboolean
_core_audio_set_property (GstCoreAudio * core_audio, AudioUnitPropertyID inID,
    void *inData, UInt32 inDataSize)
{
  OSStatus status;
  AudioUnitScope scope;
  AudioUnitElement element;

  scope = CORE_AUDIO_INNER_SCOPE (core_audio);
  element = CORE_AUDIO_ELEMENT (core_audio);

  status =
      AudioUnitSetProperty (core_audio->audiounit, inID, scope, element, inData,
      inDataSize);

  if (status != noErr) {
    GST_WARNING_OBJECT (core_audio->osxbuf,
        "Failed to set Audio Unit property: %d", (int) status);
    return FALSE;;
  }

  return TRUE;
}

/* The AudioUnit must be uninitialized before calling this */
gboolean
gst_core_audio_set_channel_layout (GstCoreAudio * core_audio,
    gint channels, GstCaps * caps)
{
  AudioChannelLayout *layout = NULL;
  gboolean ret;
  gsize layoutSize;
  gint i;
  GstStructure *structure;
  GstAudioChannelPosition positions[GST_OSX_AUDIO_MAX_CHANNEL];
  guint64 channel_mask;

  g_return_val_if_fail (channels <= GST_OSX_AUDIO_MAX_CHANNEL, FALSE);

  /* Determine the channel positions */
  structure = gst_caps_get_structure (caps, 0);
  channel_mask = 0;
  gst_structure_get (structure, "channel-mask", GST_TYPE_BITMASK, &channel_mask,
      NULL);

  if (channel_mask != 0)
    gst_audio_channel_positions_from_mask (channels, channel_mask, positions);

  /* AudioChannelLayout member mChannelDescriptions is variable-length array */
  layoutSize =
      G_STRUCT_OFFSET (AudioChannelLayout, mChannelDescriptions[channels]);
  layout = g_malloc (layoutSize);

  /* Fill out the AudioChannelLayout */
  layout->mChannelLayoutTag = kAudioChannelLayoutTag_UseChannelDescriptions;
  layout->mChannelBitmap = 0;   /* Not used */
  layout->mNumberChannelDescriptions = channels;
  for (i = 0; i < channels; i++) {
    if (channel_mask != 0) {
      layout->mChannelDescriptions[i].mChannelLabel =
          gst_audio_channel_position_to_core_audio (positions[i], i);
    } else {
      /* Discrete channel numbers are ORed into this */
      layout->mChannelDescriptions[i].mChannelLabel =
          kAudioChannelLabel_Discrete_0 | i;
    }

    /* Others unused */
    layout->mChannelDescriptions[i].mChannelFlags = kAudioChannelFlags_AllOff;
    layout->mChannelDescriptions[i].mCoordinates[0] = 0.f;
    layout->mChannelDescriptions[i].mCoordinates[1] = 0.f;
    layout->mChannelDescriptions[i].mCoordinates[2] = 0.f;
  }

  /* Sets GStreamer-ordered channel layout on the inner scope.
   * Reordering between the inner scope and outer scope is handled
   * by the Audio Unit itself. */
  ret = _core_audio_set_property (core_audio,
      kAudioUnitProperty_AudioChannelLayout, layout, layoutSize);

  g_free (layout);
  return ret;
}

/* The AudioUnit must be uninitialized before calling this */
gboolean
gst_core_audio_set_format (GstCoreAudio * core_audio,
    AudioStreamBasicDescription format)
{
  GST_DEBUG_OBJECT (core_audio->osxbuf, "Setting format for AudioUnit");

  return _core_audio_set_property (core_audio, kAudioUnitProperty_StreamFormat,
      &format, sizeof (AudioStreamBasicDescription));
}

gboolean
gst_core_audio_open_device (GstCoreAudio * core_audio, OSType sub_type,
    const gchar * adesc)
{
  AudioComponentDescription desc;
  AudioComponent comp;
  OSStatus status;
  AudioUnit unit;
  UInt32 enableIO;

  desc.componentType = kAudioUnitType_Output;
  desc.componentSubType = sub_type;
  desc.componentManufacturer = kAudioUnitManufacturer_Apple;
  desc.componentFlags = 0;
  desc.componentFlagsMask = 0;

  comp = AudioComponentFindNext (NULL, &desc);

  if (comp == NULL) {
    GST_WARNING_OBJECT (core_audio->osxbuf, "Couldn't find %s component",
        adesc);
    return FALSE;
  }

  status = AudioComponentInstanceNew (comp, &unit);

  if (status) {
    GST_ERROR_OBJECT (core_audio->osxbuf, "Couldn't open %s component %d",
        adesc, (int) status);
    return FALSE;
  }

  if (core_audio->is_src) {
    /* enable input */
    enableIO = 1;
    status = AudioUnitSetProperty (unit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, 1,   /* 1 = input element */
        &enableIO, sizeof (enableIO));

    if (status) {
      AudioComponentInstanceDispose (unit);
      GST_WARNING_OBJECT (core_audio->osxbuf, "Failed to enable input: %d",
          (int) status);
      return FALSE;
    }

    /* disable output */
    enableIO = 0;
    status = AudioUnitSetProperty (unit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, 0,  /* 0 = output element */
        &enableIO, sizeof (enableIO));

    if (status) {
      AudioComponentInstanceDispose (unit);
      GST_WARNING_OBJECT (core_audio->osxbuf, "Failed to disable output: %d",
          (int) status);
      return FALSE;
    }
  }

  GST_DEBUG_OBJECT (core_audio->osxbuf, "Created %s AudioUnit: %p", adesc,
      unit);
  core_audio->audiounit = unit;
  return TRUE;
}

AudioChannelLabel
gst_audio_channel_position_to_core_audio (GstAudioChannelPosition
    position, int channel)
{
  switch (position) {
    case GST_AUDIO_CHANNEL_POSITION_FRONT_LEFT:
      return kAudioChannelLabel_Left;
    case GST_AUDIO_CHANNEL_POSITION_FRONT_RIGHT:
      return kAudioChannelLabel_Right;
    case GST_AUDIO_CHANNEL_POSITION_REAR_CENTER:
      return kAudioChannelLabel_CenterSurround;
    case GST_AUDIO_CHANNEL_POSITION_REAR_LEFT:
      return kAudioChannelLabel_RearSurroundLeft;
    case GST_AUDIO_CHANNEL_POSITION_REAR_RIGHT:
      return kAudioChannelLabel_RearSurroundRight;
    case GST_AUDIO_CHANNEL_POSITION_LFE1:
      return kAudioChannelLabel_LFEScreen;
    case GST_AUDIO_CHANNEL_POSITION_FRONT_CENTER:
      return kAudioChannelLabel_Center;
    case GST_AUDIO_CHANNEL_POSITION_SIDE_LEFT:
      return kAudioChannelLabel_LeftSurroundDirect;
    case GST_AUDIO_CHANNEL_POSITION_SIDE_RIGHT:
      return kAudioChannelLabel_RightSurroundDirect;
    case GST_AUDIO_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER:
      return kAudioChannelLabel_LeftCenter;
    case GST_AUDIO_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER:
      return kAudioChannelLabel_RightCenter;
    case GST_AUDIO_CHANNEL_POSITION_TOP_REAR_LEFT:
      return kAudioChannelLabel_TopBackLeft;
    case GST_AUDIO_CHANNEL_POSITION_TOP_REAR_CENTER:
      return kAudioChannelLabel_TopBackCenter;
    case GST_AUDIO_CHANNEL_POSITION_TOP_REAR_RIGHT:
      return kAudioChannelLabel_TopBackRight;
    case GST_AUDIO_CHANNEL_POSITION_WIDE_LEFT:
      return kAudioChannelLabel_LeftWide;
    case GST_AUDIO_CHANNEL_POSITION_WIDE_RIGHT:
      return kAudioChannelLabel_RightWide;
    case GST_AUDIO_CHANNEL_POSITION_LFE2:
      return kAudioChannelLabel_LFE2;
    case GST_AUDIO_CHANNEL_POSITION_TOP_FRONT_LEFT:
      return kAudioChannelLabel_VerticalHeightLeft;
    case GST_AUDIO_CHANNEL_POSITION_TOP_FRONT_RIGHT:
      return kAudioChannelLabel_VerticalHeightRight;
    case GST_AUDIO_CHANNEL_POSITION_TOP_FRONT_CENTER:
      return kAudioChannelLabel_VerticalHeightCenter;
    case GST_AUDIO_CHANNEL_POSITION_TOP_CENTER:
      return kAudioChannelLabel_TopCenterSurround;
    case GST_AUDIO_CHANNEL_POSITION_SURROUND_LEFT:
      return kAudioChannelLabel_LeftSurround;
    case GST_AUDIO_CHANNEL_POSITION_SURROUND_RIGHT:
      return kAudioChannelLabel_RightSurround;

      /* Special position values */
    case GST_AUDIO_CHANNEL_POSITION_NONE:
      return kAudioChannelLabel_Discrete_0 | channel;
    case GST_AUDIO_CHANNEL_POSITION_MONO:
      return kAudioChannelLabel_Mono;

      /* Following positions are unmapped --
       * i.e. mapped to kAudioChannelLabel_Unknown: */
    case GST_AUDIO_CHANNEL_POSITION_TOP_SIDE_LEFT:
    case GST_AUDIO_CHANNEL_POSITION_TOP_SIDE_RIGHT:
    case GST_AUDIO_CHANNEL_POSITION_BOTTOM_FRONT_CENTER:
    case GST_AUDIO_CHANNEL_POSITION_BOTTOM_FRONT_LEFT:
    case GST_AUDIO_CHANNEL_POSITION_BOTTOM_FRONT_RIGHT:
    default:
      return kAudioChannelLabel_Unknown;
  }
}

/* Performs a best-effort conversion. 'channel' is used for warnings only. */
GstAudioChannelPosition
gst_core_audio_channel_label_to_gst (AudioChannelLabel label,
    int channel, gboolean warn, AudioDeviceID device_id)
{
  switch (label) {
    case kAudioChannelLabel_Left:
      return GST_AUDIO_CHANNEL_POSITION_FRONT_LEFT;
    case kAudioChannelLabel_Right:
      return GST_AUDIO_CHANNEL_POSITION_FRONT_RIGHT;
    case kAudioChannelLabel_Center:
      return GST_AUDIO_CHANNEL_POSITION_FRONT_CENTER;
    case kAudioChannelLabel_LFEScreen:
      return GST_AUDIO_CHANNEL_POSITION_LFE1;
    case kAudioChannelLabel_LeftSurround:
      return GST_AUDIO_CHANNEL_POSITION_SURROUND_LEFT;
    case kAudioChannelLabel_RightSurround:
      return GST_AUDIO_CHANNEL_POSITION_SURROUND_RIGHT;
    case kAudioChannelLabel_LeftSurroundDirect:
      return GST_AUDIO_CHANNEL_POSITION_SIDE_LEFT;
    case kAudioChannelLabel_RightSurroundDirect:
      return GST_AUDIO_CHANNEL_POSITION_SIDE_RIGHT;
    case kAudioChannelLabel_CenterSurround:
      return GST_AUDIO_CHANNEL_POSITION_REAR_CENTER;
    case kAudioChannelLabel_LeftCenter:
      return GST_AUDIO_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER;
    case kAudioChannelLabel_RightCenter:
      return GST_AUDIO_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER;
    case kAudioChannelLabel_TopBackLeft:
      return GST_AUDIO_CHANNEL_POSITION_TOP_REAR_LEFT;
    case kAudioChannelLabel_TopBackCenter:
      return GST_AUDIO_CHANNEL_POSITION_TOP_REAR_CENTER;
    case kAudioChannelLabel_TopBackRight:
      return GST_AUDIO_CHANNEL_POSITION_TOP_REAR_RIGHT;
    case kAudioChannelLabel_LeftWide:
      return GST_AUDIO_CHANNEL_POSITION_WIDE_LEFT;
    case kAudioChannelLabel_RightWide:
      return GST_AUDIO_CHANNEL_POSITION_WIDE_RIGHT;
    case kAudioChannelLabel_LFE2:
      return GST_AUDIO_CHANNEL_POSITION_LFE2;
    case kAudioChannelLabel_VerticalHeightLeft:
      return GST_AUDIO_CHANNEL_POSITION_TOP_FRONT_LEFT;
    case kAudioChannelLabel_VerticalHeightRight:
      return GST_AUDIO_CHANNEL_POSITION_TOP_FRONT_RIGHT;
    case kAudioChannelLabel_VerticalHeightCenter:
      return GST_AUDIO_CHANNEL_POSITION_TOP_FRONT_CENTER;
    case kAudioChannelLabel_RearSurroundLeft:
      return GST_AUDIO_CHANNEL_POSITION_REAR_LEFT;
    case kAudioChannelLabel_RearSurroundRight:
      return GST_AUDIO_CHANNEL_POSITION_REAR_RIGHT;
    case kAudioChannelLabel_TopCenterSurround:
      return GST_AUDIO_CHANNEL_POSITION_TOP_CENTER;

      /* Special position values */

    case kAudioChannelLabel_Mono:
      /* GST_AUDIO_CHANNEL_POSITION_MONO is only for 1-channel layouts */
      return GST_AUDIO_CHANNEL_POSITION_INVALID;
    case kAudioChannelLabel_Discrete:
      return GST_AUDIO_CHANNEL_POSITION_NONE;

      /*
         Following labels are unmapped --
         i.e. mapped to GST_AUDIO_CHANNEL_POSITION_INVALID:
       */
    case kAudioChannelLabel_LeftTotal:
    case kAudioChannelLabel_RightTotal:
    case kAudioChannelLabel_HearingImpaired:
    case kAudioChannelLabel_Narration:
    case kAudioChannelLabel_DialogCentricMix:
    case kAudioChannelLabel_CenterSurroundDirect:
    case kAudioChannelLabel_Haptic:
    default:
      if (label >> 16 != 0) {   /* kAudioChannelLabel_Discrete_N */
        /* no way to store discrete channel order */
        if (warn)
          GST_WARNING
              ("Device %d has kAudioChannelLabel_Discrete_N channels, order will be lost",
              device_id);
        return GST_AUDIO_CHANNEL_POSITION_NONE;
      } else {
        if (warn)
          GST_WARNING
              ("Device %d channel %u has unsupported label %d, skipping",
              device_id, channel, (int) label);
        return GST_AUDIO_CHANNEL_POSITION_INVALID;
      }
  }
}

void
gst_core_audio_dump_channel_layout (AudioChannelLayout * channel_layout)
{
  UInt32 i;

  GST_DEBUG ("mChannelLayoutTag: 0x%lx",
      (unsigned long) channel_layout->mChannelLayoutTag);
  GST_DEBUG ("mChannelBitmap: 0x%lx",
      (unsigned long) channel_layout->mChannelBitmap);
  GST_DEBUG ("mNumberChannelDescriptions: %lu",
      (unsigned long) channel_layout->mNumberChannelDescriptions);
  for (i = 0; i < channel_layout->mNumberChannelDescriptions; i++) {
    AudioChannelDescription *channel_desc =
        &channel_layout->mChannelDescriptions[i];
    GST_DEBUG ("  mChannelLabel: 0x%lx mChannelFlags: 0x%lx "
        "mCoordinates[0]: %f mCoordinates[1]: %f "
        "mCoordinates[2]: %f",
        (unsigned long) channel_desc->mChannelLabel,
        (unsigned long) channel_desc->mChannelFlags,
        channel_desc->mCoordinates[0], channel_desc->mCoordinates[1],
        channel_desc->mCoordinates[2]);
  }
}

#if TARGET_OS_OSX
char *
gst_core_audio_device_get_prop_str (AudioDeviceID device_id,
    AudioObjectPropertyElement prop_id)
{
  OSStatus status = noErr;
  UInt32 propertySize = 0;
  CFStringRef prop_val;
  gchar *result = NULL;

  AudioObjectPropertyAddress propAddress = {
    prop_id,
    kAudioDevicePropertyScopeOutput,
    kAudioObjectPropertyElementMain
  };

  propAddress.mScope = kAudioObjectPropertyScopeGlobal;

  /* Get the length of the device name */
  status = AudioObjectGetPropertyDataSize (device_id,
      &propAddress, 0, NULL, &propertySize);
  if (status != noErr) {
    goto beach;
  }

  /* Get the requested property */
  status = AudioObjectGetPropertyData (device_id,
      &propAddress, 0, NULL, &propertySize, &prop_val);
  if (status != noErr) {
    goto beach;
  }

  /* Convert to UTF-8 C String */
  CFIndex prop_len = CFStringGetLength (prop_val);
  CFIndex max_size =
      CFStringGetMaximumSizeForEncoding (prop_len, kCFStringEncodingUTF8) + 1;
  result = g_malloc (max_size);

  if (!CFStringGetCString (prop_val, result, max_size, kCFStringEncodingUTF8)) {
    g_free (result);
    result = NULL;
  }

  CFRelease (prop_val);

beach:
  return result;
}

UInt32
gst_core_audio_device_get_prop_uint32 (AudioDeviceID device_id,
    AudioObjectPropertyElement prop_id)
{
  OSStatus status = noErr;
  UInt32 propertySize = sizeof (UInt32);
  UInt32 result = UINT32_MAX;

  AudioObjectPropertyAddress propAddress = {
    .mSelector = prop_id,
    .mScope = kAudioObjectPropertyScopeGlobal,
    .mElement = kAudioObjectPropertyElementMain,
  };

  /* Get the requested property */
  status = AudioObjectGetPropertyData (device_id,
      &propAddress, 0, NULL, &propertySize, &result);
  if (status != noErr) {
    GST_WARNING ("Device %i, error code %i while fetching property %"
        GST_FOURCC_FORMAT, device_id, status,
        GST_FOURCC_ARGS (GUINT32_FROM_BE (prop_id)));
  }

  return result;
}
#endif
