// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "media/audio/ios/audio_session_manager_ios.h"

#import <AVFAudio/AVFAudio.h>
#import <AVFoundation/AVFoundation.h>
#import <Foundation/Foundation.h>

#include "base/strings/sys_string_conversions.h"
#include "build/build_config.h"

namespace media {

// The constant below is taken from:
// https://source.chromium.org/chromium/chromium/src/+/main:third_party/webrtc/sdk/objc/components/audio/RTCAudioSessionConfiguration.m
const int kRTCAudioSessionPreferredNumberOfChannels = 1;

// static
AudioSessionManagerIOS& AudioSessionManagerIOS::GetInstance() {
  static base::NoDestructor<AudioSessionManagerIOS> instance;
  return *instance;
}

AudioSessionManagerIOS::AudioSessionManagerIOS() {
  AVAudioSession* audio_session = [AVAudioSession sharedInstance];

  NSError* error = nil;
  AVAudioSessionCategoryOptions options;
#if !BUILDFLAG(IS_IOS_TVOS)
  options = AVAudioSessionCategoryOptionAllowBluetooth |
            AVAudioSessionCategoryOptionAllowBluetoothA2DP |
            AVAudioSessionCategoryOptionDefaultToSpeaker |
            AVAudioSessionCategoryOptionMixWithOthers;
#else
  options = 0;
#endif

  AVAudioSessionCategory category;
#if !BUILDFLAG(IS_IOS_TVOS)
  category = AVAudioSessionCategoryPlayAndRecord;
#else
  category = AVAudioSessionCategoryPlayback;
#endif

  [audio_session setCategory:category
                        mode:AVAudioSessionModeDefault
                     options:options
                       error:&error];
  if (error) {
    NSLog(@"Failed to set audio session category with error: %@.",
          error.localizedDescription);
  }

  // Find the desired input port
  NSArray* inputs = [audio_session availableInputs];
  AVAudioSessionPortDescription* builtInMic = nil;
  for (AVAudioSessionPortDescription* port in inputs) {
    if ([port.portType isEqualToString:AVAudioSessionPortBuiltInMic]) {
      builtInMic = port;
      break;
    }
  }

  [audio_session setPreferredInput:builtInMic error:nil];

  AVAudioSessionPortDescription* preferredInput =
      [audio_session preferredInput];
  if (preferredInput != nil) {
    NSArray<AVAudioSessionDataSourceDescription*>* dataSources =
        audio_session.preferredInput.dataSources;
    AVAudioSessionDataSourceDescription* newDataSource = nil;
    for (AVAudioSessionDataSourceDescription* dataSource in dataSources) {
      // Choosing AVAudioSessionOrientationBottom sets the mono channel for a
      // audio session.
      if ([dataSource.orientation isEqual:AVAudioSessionOrientationBottom]) {
        newDataSource = dataSource;
        break;
      }
    }

    if (newDataSource != nil) {
      [preferredInput setPreferredDataSource:newDataSource error:nil];
    }
  }

  // Find the desired audio output device
  AVAudioSessionRouteDescription* currentRoute = [audio_session currentRoute];
  if ([currentRoute.outputs count] > 0) {
#if !BUILDFLAG(IS_IOS_TVOS)
    AVAudioSessionPortDescription* output = currentRoute.outputs.firstObject;
    if ([output.portType isEqualToString:AVAudioSessionPortBuiltInReceiver] ||
        [output.portType isEqualToString:AVAudioSessionPortBuiltInSpeaker]) {
      error = nil;
      [audio_session overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker
                                       error:&error];
      if (error) {
        NSLog(@"Error overriding output audio port: %@",
              [error localizedDescription]);
      } else {
        NSLog(@"Set default output device to Speaker");
      }
    } else {
      error = nil;
      [audio_session overrideOutputAudioPort:AVAudioSessionPortOverrideNone
                                       error:&error];
      if (error) {
        NSLog(@"Error overriding output audio port: %@",
              [error localizedDescription]);
      } else {
        NSLog(@"Using System chosen default audio output device");
      }
    }
#else
    // tvOS manages audio output routing at the system level and does not
    // support port overrides. Hardware devices use HDMIOutput as the single
    // available route, while the simulator uses BuiltInSpeaker.
    NSLog(@"Using System chosen default audio output device");
#endif
  }

  [audio_session setActive:YES error:nil];
  [audio_session setPreferredInputNumberOfChannels:
                     kRTCAudioSessionPreferredNumberOfChannels
                                             error:nil];
}

void AudioSessionManagerIOS::SetActive(bool active) {
  AVAudioSession* audio_session = [AVAudioSession sharedInstance];
  if (active) {
    [audio_session setActive:YES error:nil];
  } else {
    [audio_session setActive:NO error:nil];
  }
}

bool AudioSessionManagerIOS::HasAudioHardware(bool is_input) {
  AVAudioSession* audio_session = [AVAudioSession sharedInstance];
  AVAudioSessionRouteDescription* route = [audio_session currentRoute];
  if (is_input) {
    // Search for an audio input hardware.
    NSArray* inputs = [route inputs];
    return [inputs count];
  }

  // Search for an audio output hardware.
  NSArray* outputs = [route outputs];
  return [outputs count];
}

void AudioSessionManagerIOS::GetAudioDeviceInfo(
    bool is_input,
    media::AudioDeviceNames* device_names) {
  if (is_input) {
    GetAudioInputDeviceInfo(device_names);
  } else {
    GetAudioOutputDeviceInfo(device_names);
  }
}

std::string AudioSessionManagerIOS::GetDefaultOutputDeviceID() {
  AVAudioSession* audio_session = [AVAudioSession sharedInstance];
  AVAudioSessionPortDescription* currentOutput =
      [audio_session currentRoute].outputs.firstObject;
  return base::SysNSStringToUTF8([currentOutput portName]);
}

std::string AudioSessionManagerIOS::GetDefaultInputDeviceID() {
  AVAudioSession* audio_session = [AVAudioSession sharedInstance];
  AVAudioSessionPortDescription* currentInput =
      [audio_session currentRoute].inputs.firstObject;
  return base::SysNSStringToUTF8([currentInput portName]);
}

double AudioSessionManagerIOS::HardwareSampleRate() {
  AVAudioSession* audio_session = [AVAudioSession sharedInstance];
  return audio_session.sampleRate;
}

double AudioSessionManagerIOS::HardwareIOBufferDuration() {
  AVAudioSession* audio_session = [AVAudioSession sharedInstance];
  return audio_session.IOBufferDuration;
}

double AudioSessionManagerIOS::HardwareLatency(bool is_input) {
  AVAudioSession* audio_session = [AVAudioSession sharedInstance];
  return is_input ? audio_session.inputLatency : audio_session.outputLatency;
}

long AudioSessionManagerIOS::GetDeviceChannels(bool is_input) {
  AVAudioSession* audio_session = [AVAudioSession sharedInstance];
  return is_input ? audio_session.inputNumberOfChannels
                  : audio_session.outputNumberOfChannels;
}

float AudioSessionManagerIOS::GetInputGain() {
  AVAudioSession* audio_session = [AVAudioSession sharedInstance];
  return audio_session.inputGain;
}

bool AudioSessionManagerIOS::SetInputGain(float volume) {
  AVAudioSession* audio_session = [AVAudioSession sharedInstance];
  if ([audio_session isInputGainSettable] == YES) {
    BOOL success = [audio_session setInputGain:volume error:nil];
    return success;
  }
  return false;
}

bool AudioSessionManagerIOS::IsInputMuted() {
#if defined(__IPHONE_17_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_17_0
  if (@available(iOS 17.0, *)) {
    AVAudioApplication* audio_application = [AVAudioApplication sharedInstance];
    return audio_application.isInputMuted;
  }
#endif
  return false;
}

bool AudioSessionManagerIOS::IsInputGainSettable() {
  AVAudioSession* audio_session = [AVAudioSession sharedInstance];
  return [audio_session isInputGainSettable] == YES;
}

// private
void AudioSessionManagerIOS::GetAudioInputDeviceInfo(
    media::AudioDeviceNames* device_names) {
  AVAudioSession* audio_session = [AVAudioSession sharedInstance];
  // Find the desired input port
  NSArray* inputs = [audio_session availableInputs];
  for (AVAudioSessionPortDescription* port in inputs) {
    device_names->emplace_back(
        std::string(base::SysNSStringToUTF8([port portName])),
        std::string(base::SysNSStringToUTF8([port UID])));
  }

  if (!device_names->empty()) {
    // Prepend the default device to the list since we always want it to be
    // on the top of the list for all platforms. There is no duplicate
    // counting here since the default device has been abstracted out before.
    device_names->push_front(media::AudioDeviceName::CreateDefault());
  }
}

void AudioSessionManagerIOS::GetAudioOutputDeviceInfo(
    media::AudioDeviceNames* device_names) {
  AVAudioSession* audio_session = [AVAudioSession sharedInstance];
  AVAudioSessionPortDescription* currentOutput =
      [audio_session currentRoute].outputs.firstObject;
  device_names->emplace_back(
      std::string(base::SysNSStringToUTF8([currentOutput portName])),
      std::string(base::SysNSStringToUTF8([currentOutput UID])));

  if (!device_names->empty()) {
    // Prepend the default device to the list since we always want it to be
    // on the top of the list for all platforms. There is no duplicate
    // counting here since the default device has been abstracted out before.
    device_names->push_front(media::AudioDeviceName::CreateDefault());
  }
}

}  // namespace media
