/*---------------------------------------------------------------------------*/
/* IosPhone.h                                                                */
/* copyright (c) innovaphone 2015                                            */
/*                                                                           */
/*---------------------------------------------------------------------------*/

#import <AVFoundation/AVFoundation.h>
//#import <AVFoundation/AVAudioSession.h>
//#import <AVFoundation/AVAudioPlayer.h>

#import "platform/platform.h"
#import "platform/IosNamespace.h"
#import "client/linux/client_main/appclient_config.h"
#import "IosPhone.h"

@class IosPhone;

extern void *ios_dsp_context;
extern IosPhone *phone_ios_ios_phone;
extern bool phone_ios_ring_in_headset;
extern void audioFocusLoss(bool audio_focus_loss);
extern void performAudioRouteChanged(const char *uuid, const char * deviceId);
extern void switch_audio_unit(bool on);
extern void request_restart_dsp();
extern void onAudioSessionActive(bool active);

@interface IosPhone()

@property (nonatomic) AVAudioPlayer *ringtonePlayer;
@property (nonatomic) bool audioRouteReported;
@property (nonatomic) NSString * reportedDeviceId;
@property (nonatomic) bool audioRouteReportDelayed;
@property (nonatomic) bool audioSessionActive;
@property (nonatomic) bool audioSessionActivated;
#if TARGET_OS_IPHONE
#else
@property (nonatomic) NSSound *ringtoneSound;
#endif
@property (nonatomic) void (* devicesChangedHandler)(void * context);
@property (nonatomic) void * devicesChangedContext;

@end
@implementation IosPhone

#if TARGET_OS_IPHONE

#define AUDIO_SESSION_PORT_PRIO_NONE                0
#define AUDIO_SESSION_PORT_PRIO_BLUETOOTH_LE        1
#define AUDIO_SESSION_PORT_PRIO_BLUETOOTH_LE_HW     2
#define AUDIO_SESSION_PORT_PRIO_BLUETOOTH_A2DP      3
#define AUDIO_SESSION_PORT_PRIO_BLUETOOTH_A2DP_HW   4
#define AUDIO_SESSION_PORT_PRIO_BLUETOOTH_HPF       5
#define AUDIO_SESSION_PORT_PRIO_BLUETOOTH_HPF_HW    6
#define AUDIO_SESSION_PORT_PRIO_USB_AUDIO           7
#define AUDIO_SESSION_PORT_PRIO_USB_AUDIO_HW        8
#define AUDIO_SESSION_PORT_PRIO_CAR_AUDIO           9
#define AUDIO_SESSION_PORT_PRIO_CAR_AUDIO_HW        10

static unsigned headsetAudioSessionPortPrio(AVAudioSessionPortDescription *portDesc)
{
    return ((portDesc == nil) ? AUDIO_SESSION_PORT_PRIO_NONE :
        ([portDesc.portType isEqualToString:AVAudioSessionPortCarAudio] ? (portDesc.hasHardwareVoiceCallProcessing ? AUDIO_SESSION_PORT_PRIO_CAR_AUDIO_HW : AUDIO_SESSION_PORT_PRIO_CAR_AUDIO) :
        ([portDesc.portType isEqualToString:AVAudioSessionPortUSBAudio] ? (portDesc.hasHardwareVoiceCallProcessing ? AUDIO_SESSION_PORT_PRIO_USB_AUDIO_HW : AUDIO_SESSION_PORT_PRIO_USB_AUDIO) :
        ([portDesc.portType isEqualToString:AVAudioSessionPortBluetoothHFP] ? (portDesc.hasHardwareVoiceCallProcessing ? AUDIO_SESSION_PORT_PRIO_BLUETOOTH_HPF_HW : AUDIO_SESSION_PORT_PRIO_BLUETOOTH_HPF) :
        ([portDesc.portType isEqualToString:AVAudioSessionPortBluetoothA2DP] ? (portDesc.hasHardwareVoiceCallProcessing ? AUDIO_SESSION_PORT_PRIO_BLUETOOTH_A2DP_HW : AUDIO_SESSION_PORT_PRIO_BLUETOOTH_A2DP) :
        ([portDesc.portType isEqualToString:AVAudioSessionPortBluetoothLE] ? (portDesc.hasHardwareVoiceCallProcessing ? AUDIO_SESSION_PORT_PRIO_BLUETOOTH_LE_HW : AUDIO_SESSION_PORT_PRIO_BLUETOOTH_LE) :
        AUDIO_SESSION_PORT_PRIO_NONE))))));
}

- (void) handleInterruption:(NSNotification *)notification
{
    NSError *error = nil;
    UInt8 interruptionType = [[notification.userInfo valueForKey:AVAudioSessionInterruptionTypeKey] intValue];

    switch (interruptionType) {
    case AVAudioSessionInterruptionTypeBegan:
        NSLog(@"Session interruption began");
        switch_audio_unit(false);
        audioFocusLoss(true);
        break;

    case AVAudioSessionInterruptionTypeEnded:
        NSLog(@"Session interruption ended");
        // make sure to activate the session
        [[AVAudioSession sharedInstance] setActive:YES error:&error];
        if (error != nil) {
            NSLog(@"AVAudioSession set active failed with error: %@", error);
        }
        switch_audio_unit(true);
        audioFocusLoss(false);
        break;

    default:
        break;
    }
}

- (void) audio_route_change_runnable:(id)sender
{
    unsigned i;

    AVAudioSession *sessionInstance = [AVAudioSession sharedInstance];
    AVAudioSessionRouteDescription *route = sessionInstance.currentRoute;
    if (route != nil) {
        NSString * audioDeviceId = @AUDIO_DEVICE_ID_HANDSET;
        if (route.inputs != nil) {
            for (i = 0; i < route.inputs.count; i++) {
                AVAudioSessionPortDescription *portDesc = [route.inputs objectAtIndex:i];
                if (headsetAudioSessionPortPrio(portDesc) != AUDIO_SESSION_PORT_PRIO_NONE) {
                    audioDeviceId = [sessionInstance respondsToSelector:@selector(setPreferredInput:error:)] ? portDesc.UID : @AUDIO_DEVICE_ID_HEADSET;
                }
            }
        }
        if (route.outputs != nil) {
            for (i = 0; i < route.outputs.count; i++) {
                AVAudioSessionPortDescription *portDesc = [route.outputs objectAtIndex:i];
                if (headsetAudioSessionPortPrio(portDesc) != AUDIO_SESSION_PORT_PRIO_NONE) {
                    audioDeviceId = [sessionInstance respondsToSelector:@selector(setPreferredInput:error:)] ? portDesc.UID : @AUDIO_DEVICE_ID_HEADSET;
                }
                else if ([portDesc.portType isEqualToString:AVAudioSessionPortBuiltInSpeaker]) {
                    audioDeviceId = @AUDIO_DEVICE_ID_HANDSFREE;
                }
            }
        }
        if (!self.audioRouteReported
         || (self.reportedDeviceId == nil)
         || ![audioDeviceId isEqualToString:self.reportedDeviceId]) {

            performAudioRouteChanged(NULL, [audioDeviceId cStringUsingEncoding:NSUTF8StringEncoding]);

            self.audioRouteReported = true;
            self.reportedDeviceId = audioDeviceId;
        }
    }
    self.audioRouteReportDelayed = false;
}

- (void) handleRouteChange:(NSNotification *)notification
{
    UInt8 reasonValue = [[notification.userInfo valueForKey:AVAudioSessionRouteChangeReasonKey] intValue];

    AVAudioSession *sessionInstance = [AVAudioSession sharedInstance];
    AVAudioSessionRouteDescription *route = sessionInstance.currentRoute;
/*
    AVAudioSessionRouteDescription *routeDescription = [notification.userInfo valueForKey:AVAudioSessionRouteChangePreviousRouteKey];

    switch (reasonValue) {
    case AVAudioSessionRouteChangeReasonNewDeviceAvailable:
        NSLog(@"Route change: NewDeviceAvailable old route %@->%@", routeDescription, route);
        break;
    case AVAudioSessionRouteChangeReasonOldDeviceUnavailable:
        NSLog(@"Route change: OldDeviceUnavailable old route %@->%@", routeDescription, route);
        break;
    case AVAudioSessionRouteChangeReasonCategoryChange:
        NSLog(@"Route change: CategoryChange %@ old route %@->%@", [AVAudioSession sharedInstance].category, routeDescription, route);
        break;
    case AVAudioSessionRouteChangeReasonOverride:
        NSLog(@"Route change: Override old route %@->%@", routeDescription, route);
        break;
    case AVAudioSessionRouteChangeReasonWakeFromSleep:
        NSLog(@"Route change: WakeFromSleep old route %@->%@", routeDescription, route);
        break;
    case AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory:
        NSLog(@"Route change: NoSuitableRouteForCategory old route %@->%@", routeDescription, route);
        break;
    default:
        NSLog(@"Route change: ReasonUnknown old route %@->%@", routeDescription, route);
    }
*/
    if ((reasonValue == AVAudioSessionRouteChangeReasonNewDeviceAvailable)
     || (reasonValue == AVAudioSessionRouteChangeReasonOldDeviceUnavailable)) {
        if (self.devicesChangedHandler) {
            self.devicesChangedHandler(self.devicesChangedContext);
        }
    }

    if (route != nil) {
        if (!self.audioRouteReportDelayed) {
            [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(audio_route_change_runnable:) object:nil];
            [self performSelector:@selector(audio_route_change_runnable:) withObject:nil afterDelay:0.1];
        }
    }
}

- (void) onMediaServerReset:(id)context
{
    request_restart_dsp();
}

- (void) handleMediaServerReset:(NSNotification *)notification
{
    NSLog(@"Media server has reset");
    
    usleep(25000); //wait here for some time to ensure that we don't delete these objects while they are being accessed elsewhere

    if (self.ringtonePlayer != nil) {
        [self.ringtonePlayer stop];
        self.ringtonePlayer = nil;
    }
#if TARGET_OS_IPHONE
#else
    if (self.ringtoneSound != nil) {
        [self.ringtoneSound stop];
        self.ringtoneSound = nil;
    }
#endif
    [self performSelectorOnMainThread:@selector(onMediaServerReset:) withObject:nil waitUntilDone:NO];
}

- (void) setAudioSessionDevice_:(NSString *) deviceId 
{
    AVAudioSession *sessionInstance = [AVAudioSession sharedInstance];
    NSError *error = nil;
    unsigned i;

    AVAudioSessionRouteDescription *route = sessionInstance.currentRoute;
    NSString * audioDeviceId = @AUDIO_DEVICE_ID_HANDSET;
    if (route) {
        if (route.inputs != nil) {
            for (i = 0; i < route.inputs.count; i++) {
                AVAudioSessionPortDescription *portDesc = [route.inputs objectAtIndex:i];
                if (headsetAudioSessionPortPrio(portDesc) != AUDIO_SESSION_PORT_PRIO_NONE) {
                    audioDeviceId = [sessionInstance respondsToSelector:@selector(setPreferredInput:error:)] ? portDesc.UID : @AUDIO_DEVICE_ID_HEADSET;
                }
            }
        }
        if (route.outputs != nil) {
            for (i = 0; i < route.outputs.count; i++) {
                AVAudioSessionPortDescription *portDesc = [route.outputs objectAtIndex:i];
                if (headsetAudioSessionPortPrio(portDesc) != AUDIO_SESSION_PORT_PRIO_NONE) {
                    audioDeviceId = [sessionInstance respondsToSelector:@selector(setPreferredInput:error:)] ? portDesc.UID : @AUDIO_DEVICE_ID_HEADSET;
                }
                else if ([portDesc.portType isEqualToString:AVAudioSessionPortBuiltInSpeaker]) {
                    audioDeviceId = @AUDIO_DEVICE_ID_HANDSFREE;
                }
            }
        }
    }
    bool speakerphoneOn = [deviceId isEqualToString:@AUDIO_DEVICE_ID_HANDSFREE];
    bool headsetOn = ![deviceId isEqualToString:@AUDIO_DEVICE_ID_HANDSET] && !speakerphoneOn;
    if (!speakerphoneOn && [audioDeviceId isEqualToString:@AUDIO_DEVICE_ID_HANDSFREE]) {
        NSError *error = nil;
/*
        UInt32 audioRouteOverride = speakerphoneOn ? kAudioSessionOverrideAudioRoute_Speaker : kAudioSessionOverrideAudioRoute_None;
        AudioSessionSetProperty(kAudioSessionProperty_OverrideCategoryDefaultToSpeaker, sizeof(audioRouteOverride), &audioRouteOverride);
*/
        UInt32 audioRouteOverride = speakerphoneOn ? kAudioSessionOverrideAudioRoute_Speaker : kAudioSessionOverrideAudioRoute_None;
        AudioSessionSetProperty(kAudioSessionProperty_OverrideAudioRoute, sizeof(audioRouteOverride), &audioRouteOverride);
        if ([sessionInstance respondsToSelector:@selector(overrideOutputAudioPort:error:)]) {
            if (![[AVAudioSession sharedInstance] overrideOutputAudioPort:speakerphoneOn ? AVAudioSessionPortOverrideSpeaker : AVAudioSessionPortOverrideNone error:&error]) {
                NSLog(@"Couldn't set speakerphone %u %@", speakerphoneOn, error);
            }
        }
    }
    AVAudioSessionPortDescription *foundPortDesc = nil;
    if (headsetOn) {
        if ([sessionInstance respondsToSelector:@selector(overrideOutputAudioPort:error:)]) {
            NSArray<AVAudioSessionPortDescription *> *outputs = sessionInstance.currentRoute.outputs;
            if (outputs) {
                for (i = 0; i < outputs.count; i++) {
                    AVAudioSessionPortDescription *portDesc = [outputs objectAtIndex:i];
                    if ([portDesc.UID isEqualToString:deviceId]
                     || (((foundPortDesc == nil)
                       || ![foundPortDesc.UID isEqualToString:deviceId])
                      && (headsetAudioSessionPortPrio(portDesc) > headsetAudioSessionPortPrio(foundPortDesc)))) {
                        foundPortDesc = portDesc;
                    }
//                    NSLog(@"outputs %@ %@ %@", portDesc.portType, portDesc.portName, portDesc.UID);
                }
            }
            NSArray<AVAudioSessionPortDescription *> *inputs = sessionInstance.currentRoute.inputs;
            if (inputs) {
                for (i = 0; i < inputs.count; i++) {
                    AVAudioSessionPortDescription *portDesc = [inputs objectAtIndex:i];
                    if ([portDesc.UID isEqualToString:deviceId]
                     || (((foundPortDesc == nil)
                       || ![foundPortDesc.UID isEqualToString:deviceId])
                      && (headsetAudioSessionPortPrio(portDesc) > headsetAudioSessionPortPrio(foundPortDesc)))) {
                        foundPortDesc = portDesc;
                    }
//                    NSLog(@"inputs %@ %@ %@", portDesc.portType, portDesc.portName, portDesc.UID);
                }
            }
        }
        if ([sessionInstance respondsToSelector:@selector(setPreferredInput:error:)]) {
            NSArray<AVAudioSessionPortDescription *> *availableInputs = sessionInstance.availableInputs;
            if (availableInputs) {
                for (i = 0; i < availableInputs.count; i++) {
                    AVAudioSessionPortDescription *portDesc = [availableInputs objectAtIndex:i];
                    if ([portDesc.UID isEqualToString:deviceId]
                     || (((foundPortDesc == nil)
                       || ![foundPortDesc.UID isEqualToString:deviceId])
                      && (headsetAudioSessionPortPrio(portDesc) > headsetAudioSessionPortPrio(foundPortDesc)))) {
                        foundPortDesc = portDesc;
                    }
//                    NSLog(@"availableInputs %@ %@ %@", portDesc.portType, portDesc.portName, portDesc.UID);
                }
            }
        }
    }
    if (route
     && ((headsetOn ^ (![audioDeviceId isEqualToString:@AUDIO_DEVICE_ID_HANDSET] && ![audioDeviceId isEqualToString:@AUDIO_DEVICE_ID_HANDSFREE]))
      || (headsetOn
       && ![deviceId isEqualToString:@AUDIO_DEVICE_ID_DEFAULT]
       && ![deviceId isEqualToString:audioDeviceId]
       && (foundPortDesc != nil)
       && [foundPortDesc.UID isEqualToString:deviceId]))) {

        if ([sessionInstance respondsToSelector:@selector(setPreferredInput:error:)]) {
            NSArray<AVAudioSessionPortDescription *> *availableInputs = sessionInstance.availableInputs;
            if (availableInputs) {
                i = 0;
                for (i = 0; i < availableInputs.count; i++) {
                    AVAudioSessionPortDescription *portDesc = [availableInputs objectAtIndex:i];
                    if ((headsetOn
                      && ([portDesc.UID isEqualToString:deviceId]
                       || (((foundPortDesc == nil)
                         || ![foundPortDesc.UID isEqualToString:deviceId])
                        && (headsetAudioSessionPortPrio(portDesc) > headsetAudioSessionPortPrio(foundPortDesc)))))
                     || (!headsetOn
                      && [portDesc.portType isEqualToString:AVAudioSessionPortBuiltInMic])) {
                        foundPortDesc = portDesc;
                    }
                }
            }
            NSLog(@"setPreferredInput %@", foundPortDesc);
            if ((foundPortDesc == nil) || ![sessionInstance setPreferredInput:foundPortDesc error:&error]) {
                NSLog(@"Couldn't set headset %u %@", headsetOn, error);
            }
        }
        if (!headsetOn) {
            self.audioRouteReportDelayed = true;
            [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(audio_route_change_runnable:) object:nil];
            [self performSelector:@selector(audio_route_change_runnable:) withObject:nil afterDelay:1];
        }
    }
    if (speakerphoneOn && ![audioDeviceId isEqualToString:@AUDIO_DEVICE_ID_HANDSFREE]) {
        NSError *error = nil;
/*
        UInt32 audioRouteOverride = speakerphoneOn ? kAudioSessionOverrideAudioRoute_Speaker : kAudioSessionOverrideAudioRoute_None;
        AudioSessionSetProperty(kAudioSessionProperty_OverrideCategoryDefaultToSpeaker, sizeof(audioRouteOverride), &audioRouteOverride);
*/
        UInt32 audioRouteOverride = speakerphoneOn ? kAudioSessionOverrideAudioRoute_Speaker : kAudioSessionOverrideAudioRoute_None;
        AudioSessionSetProperty(kAudioSessionProperty_OverrideAudioRoute, sizeof(audioRouteOverride), &audioRouteOverride);
        if ([sessionInstance respondsToSelector:@selector(overrideOutputAudioPort:error:)]) {
            if (![[AVAudioSession sharedInstance] overrideOutputAudioPort:speakerphoneOn ? AVAudioSessionPortOverrideSpeaker : AVAudioSessionPortOverrideNone error:&error]) {
                NSLog(@"Couldn't set speakerphone %u %@", speakerphoneOn, error);
            }
        }
    }
    NSLog(@"setAudioSessionDevice %@ %@ %@ %@", deviceId, audioDeviceId, route, (foundPortDesc ? foundPortDesc.portType : @"(null)"));
}

#endif

- (void) ringtonePlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag
{
}

- (void) ringtonePlayerDecodeErrorDidOccur:(AVAudioPlayer *)player error:(NSError *)error
{
    NSLog(@"Audio player decoding error %@", error);
}

@end

bool isEarphonePlugged()
{
#if TARGET_OS_IPHONE
    AVAudioSession *sessionInstance = [AVAudioSession sharedInstance];
    unsigned i;
    bool plugged = false;

    if ([sessionInstance respondsToSelector:@selector(overrideOutputAudioPort:error:)]) {
        NSArray<AVAudioSessionPortDescription *> *outputs = sessionInstance.currentRoute.outputs;
        if (outputs) {
            for (i = 0; i < outputs.count; i++) {
                AVAudioSessionPortDescription *portDesc = [outputs objectAtIndex:i];
                if ([portDesc.portType isEqualToString:AVAudioSessionPortHeadsetMic]
                 || [portDesc.portType isEqualToString:AVAudioSessionPortHeadphones]) {
                    plugged = true;
                }
            }
        }
        NSArray<AVAudioSessionPortDescription *> *inputs = sessionInstance.currentRoute.inputs;
        if (inputs) {
            for (i = 0; i < inputs.count; i++) {
                AVAudioSessionPortDescription *portDesc = [inputs objectAtIndex:i];
                if ([portDesc.portType isEqualToString:AVAudioSessionPortHeadsetMic]
                 || [portDesc.portType isEqualToString:AVAudioSessionPortHeadphones]) {
                    plugged = true;
                }
            }
        }
    }
    if ([sessionInstance respondsToSelector:@selector(setPreferredInput:error:)]) {
        NSArray<AVAudioSessionPortDescription *> *availableInputs = sessionInstance.availableInputs;
        if (availableInputs) {
            for (i = 0; i < availableInputs.count; i++) {
                AVAudioSessionPortDescription *portDesc = [availableInputs objectAtIndex:i];
                if ([portDesc.portType isEqualToString:AVAudioSessionPortHeadsetMic]
                 || [portDesc.portType isEqualToString:AVAudioSessionPortHeadphones]) {
                    plugged = true;
                }
            }
        }
    }
    return plugged;
#else
    return false;
#endif
}

bool isBluetoothActive()
{
#if TARGET_OS_IPHONE
    AVAudioSession *sessionInstance = [AVAudioSession sharedInstance];
    unsigned i;
    bool speakerEnabled = false;
    bool active = false;

    AVAudioSessionRouteDescription * route = sessionInstance.currentRoute;
    if ((route != nil) && (route.outputs != nil)) {
        for (i = 0; i < route.outputs.count; i++) {
            AVAudioSessionPortDescription *portDesc = [route.outputs objectAtIndex:i];
            if ([portDesc.portType isEqualToString:AVAudioSessionPortBuiltInSpeaker]) {
                speakerEnabled = true;
            }
        }
    }
    if ([sessionInstance respondsToSelector:@selector(overrideOutputAudioPort:error:)]) {
        NSArray<AVAudioSessionPortDescription *> *outputs = sessionInstance.currentRoute.outputs;
        if (outputs) {
            for (i = 0; i < outputs.count; i++) {
                AVAudioSessionPortDescription *portDesc = [outputs objectAtIndex:i];
                if (headsetAudioSessionPortPrio(portDesc) != AUDIO_SESSION_PORT_PRIO_NONE) {
                    if (!speakerEnabled) {
                        active = true;
                    }
                }
//                NSLog(@"outputs %@ %@ %@", portDesc.portType, portDesc.portName, portDesc.UID);
            }
        }
        NSArray<AVAudioSessionPortDescription *> *inputs = sessionInstance.currentRoute.inputs;
        if (inputs) {
            for (i = 0; i < inputs.count; i++) {
                AVAudioSessionPortDescription *portDesc = [inputs objectAtIndex:i];
                if (headsetAudioSessionPortPrio(portDesc) != AUDIO_SESSION_PORT_PRIO_NONE) {
                    if (!speakerEnabled) {
                        active = true;
                    }
                }
//                NSLog(@"inputs %@ %@ %@", portDesc.portType, portDesc.portName, portDesc.UID);
            }
        }
    }
    if ([sessionInstance respondsToSelector:@selector(setPreferredInput:error:)]) {
        NSArray<AVAudioSessionPortDescription *> *availableInputs = sessionInstance.availableInputs;
        if (availableInputs) {
            for (i = 0; i < availableInputs.count; i++) {
                AVAudioSessionPortDescription *portDesc = [availableInputs objectAtIndex:i];
                if (headsetAudioSessionPortPrio(portDesc) != AUDIO_SESSION_PORT_PRIO_NONE) {
                    if (!speakerEnabled) {
                        active = true;
                    }
                }
//                NSLog(@"availableInputs %@ %@ %@", portDesc.portType, portDesc.portName, portDesc.UID);
            }
        }
    }
    return active;
#else
    return false;
#endif
}

#if TARGET_OS_IPHONE

static char * audioSessionDeviceId = NULL;

OSStatus setAudioSessionDevice(const char * deviceId)
{
    NSString * deviceIdString = [NSString stringWithCString:deviceId encoding:NSUTF8StringEncoding];
    dispatch_async(dispatch_get_main_queue(), ^
        { [phone_ios_ios_phone setAudioSessionDevice_:deviceIdString]; });
    free(audioSessionDeviceId);
    audioSessionDeviceId = _strdup(deviceId);
    return noErr;
}

void getAudioSessionDevices(void (* add)(void * context, const char * id, const char * name, bool speakerphone, bool headset, bool voiceCommunication), void * context)
{
    unsigned prios[1024];
    unsigned i, k, n;

    AVAudioSession *sessionInstance = [AVAudioSession sharedInstance];
    add(context, AUDIO_DEVICE_ID_HANDSET, AUDIO_DEVICE_ID_HANDSET, false, false, true);
    add(context, AUDIO_DEVICE_ID_HANDSFREE, AUDIO_DEVICE_ID_HANDSFREE, true, false, true);
    if ([sessionInstance respondsToSelector:@selector(setPreferredInput:error:)]) {
        NSArray<AVAudioSessionPortDescription *> *availableInputs = sessionInstance.availableInputs;
        if (availableInputs) {
            n = AUDIO_SESSION_PORT_PRIO_NONE;
            for (i = 0; i < availableInputs.count; i++) {
                AVAudioSessionPortDescription *portDesc = [availableInputs objectAtIndex:i];
                prios[i] = headsetAudioSessionPortPrio(portDesc);
                if (prios[i] > n) {
                    n = prios[i];
                }
            }
            n++;
            do {
                k = AUDIO_SESSION_PORT_PRIO_NONE;
                for (i = 0; i < availableInputs.count; i++) {
                    if ((prios[i] > k) && (prios[i] < n)) {
                        k = prios[i];
                    }
                }
                if (k != AUDIO_SESSION_PORT_PRIO_NONE) {
                    for (i = 0; i < availableInputs.count; i++) {
                        if (prios[i] == k) {
                            AVAudioSessionPortDescription *portDesc = [availableInputs objectAtIndex:i];
                            NSLog(@"getAudioSessionDevices[%u] %@ %@ %@ %u", i, portDesc.UID, portDesc.portName, portDesc.portType, portDesc.hasHardwareVoiceCallProcessing);
                            add(context,
                                [portDesc.UID cStringUsingEncoding:NSUTF8StringEncoding],
                                [portDesc.portName cStringUsingEncoding:NSUTF8StringEncoding],
                                false,
                                true,
                                portDesc.hasHardwareVoiceCallProcessing || [portDesc.portType isEqualToString:AVAudioSessionPortCarAudio]);
                        }
                    }
                    n = k;
                }
            }
            while (k != AUDIO_SESSION_PORT_PRIO_NONE);
        }
    }
    else {
        add(context, AUDIO_DEVICE_ID_HEADSET, AUDIO_DEVICE_ID_HEADSET, false, true, true);
    }
}

#endif

OSStatus configureAudioSession(unsigned int buffer_interval_8khz, unsigned int sample_rate)
{
#if TARGET_OS_IPHONE
    NSError *error = nil;
    OSStatus os_status;

    audioFocusLoss(false);
    isBluetoothActive();
    phone_ios_ios_phone.audioRouteReported = false;

    // Configure the audio session
    AVAudioSession *sessionInstance = [AVAudioSession sharedInstance];

    // we are going to play and record so we pick that category
    if (([sessionInstance respondsToSelector:NSSelectorFromString(@"setCategory:mode:options:error:")]
      && [sessionInstance setCategory:AVAudioSessionCategoryPlayAndRecord mode:AVAudioSessionModeVoiceChat
          options:AVAudioSessionCategoryOptionAllowBluetooth | AVAudioSessionCategoryOptionAllowBluetoothA2DP error:&error])
     || ([sessionInstance respondsToSelector:@selector(setCategory:withOptions:error:)]
      && [sessionInstance setCategory:AVAudioSessionCategoryPlayAndRecord
          withOptions:AVAudioSessionCategoryOptionAllowBluetooth error:&error])
     || [sessionInstance setCategory:AVAudioSessionCategoryPlayAndRecord error:&error]) {
    }
    else {
        NSLog(@"Couldn't set audio session category %@", error);
        return (OSStatus)(error.code);
    }

    UInt32 audioSessionMode = kAudioSessionMode_VoiceChat;
    os_status = AudioSessionSetProperty(kAudioSessionProperty_Mode, sizeof(audioSessionMode), &audioSessionMode);
    if (os_status != noErr) {
        NSLog(@"Couldn't set VoiceChat on audio session %d", (int) os_status);
        return os_status;
    }

    UInt32 enableBluetooth = true;
    os_status = AudioSessionSetProperty(kAudioSessionProperty_OverrideCategoryEnableBluetoothInput, sizeof(enableBluetooth), &enableBluetooth);
    if (os_status != noErr) {
        NSLog(@"Couldn't enable Bluetooth on audio session %d", (int) os_status);
        return os_status;
    }

    // set the buffer duration
    NSTimeInterval bufferDuration = ((double) buffer_interval_8khz) / 8000.0;
    if (![sessionInstance setPreferredIOBufferDuration:bufferDuration error:&error]) {
        NSLog(@"Couldn't set audio session interval %@", error);
        return (OSStatus)(error.code);
    }

    // set the session's sample rate
    if (![sessionInstance setPreferredSampleRate:sample_rate error:&error]) {
        NSLog(@"Couldn't set audio session sample rate %@", error);
        return (OSStatus)(error.code);
    }

    // add interruption handler
    [[NSNotificationCenter defaultCenter] addObserver:phone_ios_ios_phone
                                             selector:@selector(handleInterruption:)
                                                 name:AVAudioSessionInterruptionNotification
                                               object:sessionInstance];

    // we don't do anything special in the route change notification
    [[NSNotificationCenter defaultCenter] addObserver:phone_ios_ios_phone
                                             selector:@selector(handleRouteChange:)
                                                 name:AVAudioSessionRouteChangeNotification
                                               object:sessionInstance];

    // if media services are reset, we need to rebuild our audio chain
    [[NSNotificationCenter defaultCenter] addObserver:phone_ios_ios_phone
                                             selector:@selector(handleMediaServerReset:)
                                                 name:AVAudioSessionMediaServicesWereResetNotification
                                               object:sessionInstance];

#endif
    return noErr;
}


OSStatus activateAudioSession()
{
#if TARGET_OS_IPHONE
    NSError *error = nil;
    AVAudioSession *sessionInstance = [AVAudioSession sharedInstance];

    // activate the audio session
    if (![sessionInstance setActive:YES error:&error]) {
        NSLog(@"Couldn't set audio session active %@", error);
        return (OSStatus)(error.code);
    }
    phone_ios_ios_phone.audioSessionActive = true;
    onAudioSessionActive(true);

    setAudioSessionDevice(audioSessionDeviceId ? audioSessionDeviceId : "");
#endif
    return noErr;
}


OSStatus setupAudioSession(unsigned int buffer_interval_8khz, unsigned int sample_rate)
{
    OSStatus os_status;

    os_status = configureAudioSession(buffer_interval_8khz, sample_rate);
    if (os_status != noErr) {
        return os_status;
    }

    return activateAudioSession();
}


OSStatus releaseAudioSession()
{
#if TARGET_OS_IPHONE
    NSError *error = nil;

    if (![[AVAudioSession sharedInstance] setActive:NO error:&error]) {
        NSLog(@"Couldn't set audio session inactive %@", error);
        return (OSStatus)(error.code);
    }
    phone_ios_ios_phone.audioSessionActive = false;
    onAudioSessionActive(false);
#endif
    return noErr;
}

void audioSessionActivated(bool activated)
{
    phone_ios_ios_phone.audioSessionActivated = activated;
}

#if TARGET_OS_IPHONE
void startListenAudioSessionDevices(void (* changed)(void * context), void * context)
{
    phone_ios_ios_phone.devicesChangedHandler = changed;
    phone_ios_ios_phone.devicesChangedContext = context;
}

void stopListenAudioSessionDevices()
{
    phone_ios_ios_phone.devicesChangedHandler = NULL;
}
#endif


static SystemSoundID ringtoneSoundID = 0;
static bool ringtoneAllowVibrate = false;

void ringtonePlayoutComplete(SystemSoundID ssID, void *clientData)
{
    NSLog(@"AudioServicesSystemSoundCompletionProc %u %u", ringtoneSoundID, ringtoneAllowVibrate);
    if (ringtoneSoundID) {
        if (ringtoneAllowVibrate) {
            AudioServicesPlayAlertSound(ringtoneSoundID);
        }
        else {
            AudioServicesPlaySystemSound(ringtoneSoundID);
        }
    }
}

OSStatus releaseRingtonePlayer()
{
    if (ringtoneSoundID) {
        NSLog(@"Stopping ringtone");
        AudioServicesRemoveSystemSoundCompletion(ringtoneSoundID);
        AudioServicesDisposeSystemSoundID(ringtoneSoundID);
        ringtoneSoundID = 0;
    }
    if (phone_ios_ios_phone.ringtonePlayer != nil) {
        [phone_ios_ios_phone.ringtonePlayer stop];
        phone_ios_ios_phone.ringtonePlayer = nil;
    }
#if TARGET_OS_IPHONE
    if (((audioSessionDeviceId == NULL) || (strcmp(audioSessionDeviceId, AUDIO_DEVICE_ID_HANDSFREE) != 0)) && !phone_ios_ios_phone.audioSessionActivated && !phone_ios_ios_phone.audioSessionActive) {
        [[AVAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:nil];
    }
#else
    if (phone_ios_ios_phone.ringtoneSound != nil) {
        [phone_ios_ios_phone.ringtoneSound stop];
        phone_ios_ios_phone.ringtoneSound = nil;
    }
#endif
    return noErr;
}

OSStatus createRingtonePlayerEx(const char *device, const char *melody, bool allow_vibrate, bool notification_tone)
{
    OSStatus os_status;
    NSString *melodyString = nil;
    NSString *ringtonePath = nil;
    int i;

    releaseRingtonePlayer();
    if (melody != nil) {
        melodyString = [NSString stringWithCString:melody encoding:NSUTF8StringEncoding];
        if ([melodyString hasPrefix:@"file://"]) {
            ringtonePath = [melodyString substringFromIndex:7];
        }
    }

    NSFileManager *fileMan = [NSFileManager defaultManager];
    NSString *documentDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
    NSString *ringtonesPath = [documentDir stringByAppendingPathComponent:notification_tone ? @"NotificationTones" : @"Ringtones"];
    NSArray *files = [fileMan contentsOfDirectoryAtPath:ringtonesPath error:NULL];
    i = 0;
    while ((i < files.count) && (ringtonePath == nil)) {
        NSString *s = (NSString *)[files objectAtIndex:i];
        NSUInteger start = s.length;
        NSUInteger end = s.length;
        while ((start != 0) && ([s characterAtIndex:start - 1] != '.') && ([s characterAtIndex:start - 1] != '/')) {
            start--;
        }
        if (start != 0) {
            if ([s characterAtIndex:start - 1] == '.') {
                end = start - 1;
                do {
                    start--;
                }
                while ((start != 0) && ([s characterAtIndex:start - 1] != '/'));
            }
        }
        if ((melodyString == nil) || [melodyString isEqualToString:[s substringWithRange:NSMakeRange(start, end - start)]]) {
            ringtonePath = [ringtonesPath stringByAppendingPathComponent:s];
        }
        i++;
    }

    NSArray *tunesPaths = [[NSBundle mainBundle] pathsForResourcesOfType:nil inDirectory:notification_tone ? @"NotificationTones" : @"Ringtones"];
    i = 0;
    while ((i < tunesPaths.count) && (ringtonePath == nil)) {
        NSString *s = (NSString *)[tunesPaths objectAtIndex:i];
        NSUInteger start = s.length;
        NSUInteger end = s.length;
        while ((start != 0) && ([s characterAtIndex:start - 1] != '.') && ([s characterAtIndex:start - 1] != '/')) {
            start--;
        }
        if (start != 0) {
            if ([s characterAtIndex:start - 1] == '.') {
                end = start - 1;
                do {
                    start--;
                }
                while ((start != 0) && ([s characterAtIndex:start - 1] != '/'));
            }
        }
        if (![[s substringFromIndex:end] isEqualToString:@".mak"]
         && ((melodyString == nil) || [melodyString isEqualToString:[s substringWithRange:NSMakeRange(start, end - start)]])) {
            ringtonePath = s;
        }
        i++;
    }

#if TARGET_OS_IPHONE
    if (ringtonePath == nil) {
        ringtonePath = [tunesPaths objectAtIndex:0];
    }
    if (ringtonePath != nil) {
        NSLog(notification_tone ? @"Playing notification tone %d %d \"%@\"" : @"Starting ringtone %d %d \"%@\"", allow_vibrate, isEarphonePlugged(), ringtonePath);
        NSURL *fileURL = [[NSURL alloc] initFileURLWithPath:ringtonePath];

        NSError* error = nil;
        if (!phone_ios_ios_phone.audioSessionActivated && !phone_ios_ios_phone.audioSessionActive) {
            [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:&error];
        }
        if (error == nil) {

            if (!phone_ios_ios_phone.audioSessionActivated && !phone_ios_ios_phone.audioSessionActive) {
                UInt32 audioSessionMode = kAudioSessionMode_VoiceChat;
                os_status = AudioSessionSetProperty(kAudioSessionProperty_Mode, sizeof(audioSessionMode), &audioSessionMode);
                if (os_status != noErr) {
                    NSLog(@"Couldn't set VoiceChat on audio session %d", (int) os_status);
                }

                UInt32 enableBluetooth = true;
                os_status = AudioSessionSetProperty(kAudioSessionProperty_OverrideCategoryEnableBluetoothInput, sizeof(enableBluetooth), &enableBluetooth);
                if (os_status != noErr) {
                    NSLog(@"Couldn't enable Bluetooth on audio session %d", (int) os_status);
                }
            }

            ringtoneAllowVibrate = allow_vibrate;
            AudioServicesCreateSystemSoundID((__bridge CFURLRef)fileURL, &ringtoneSoundID);
            if (!notification_tone) {
                os_status = AudioServicesAddSystemSoundCompletion(ringtoneSoundID, NULL, NULL, ringtonePlayoutComplete, NULL);
                if (os_status != noErr) {
                    NSLog(@"AudioServicesAddSystemSoundCompletion failed %d", (int) os_status);
                }
            }
            if (allow_vibrate) {
                AudioServicesPlayAlertSound(ringtoneSoundID);
            }
            else {
                AudioServicesPlaySystemSound(ringtoneSoundID);
            }

            if (isEarphonePlugged() || isBluetoothActive()) {
                if (!phone_ios_ring_in_headset && !phone_ios_ios_phone.audioSessionActivated && !phone_ios_ios_phone.audioSessionActive) {
                    [[AVAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:nil];
                }
                phone_ios_ios_phone.ringtonePlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:fileURL error:nil];
                phone_ios_ios_phone.ringtonePlayer.numberOfLoops = -1;
                [phone_ios_ios_phone.ringtonePlayer prepareToPlay];
                [phone_ios_ios_phone.ringtonePlayer setVolume:0.0];
                [phone_ios_ios_phone.ringtonePlayer setDelegate:phone_ios_ios_phone];
                [phone_ios_ios_phone.ringtonePlayer play];
            }
        }
    }
#else
    if ((ringtonePath == nil) && !allow_vibrate) {
        ringtonePath = [tunesPaths objectAtIndex:0];
    }
    if (ringtonePath != nil) {
        NSLog(notification_tone ? @"Playing notification tone %d %d \"%@\"" : @"Starting ringtone %d %d \"%@\"", allow_vibrate, isEarphonePlugged(), ringtonePath);
        NSURL *fileURL = [[NSURL alloc] initFileURLWithPath:ringtonePath];

        UInt32 outputUIDSize = sizeof(CFStringRef);
        CFStringRef outputUID = NULL;
        CFIndex outputDeviceUIDSize;
        char *outputDeviceUIDBuf;
        const char *outputDeviceUID = NULL;
        AudioDeviceID outputDeviceID = 0;
        UInt32 param = sizeof(AudioDeviceID);
        AudioObjectPropertyAddress ioProperties = { kAudioHardwarePropertyDefaultSystemOutputDevice, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMaster };
	    os_status = AudioObjectGetPropertyData(kAudioObjectSystemObject, &ioProperties, 0, NULL, &param, &outputDeviceID);
        if(os_status == noErr) {
            // get output device UID
            ioProperties.mSelector = kAudioDevicePropertyDeviceUID;
            ioProperties.mScope = kAudioObjectPropertyScopeOutput;
            os_status = AudioObjectGetPropertyData(outputDeviceID, &ioProperties, 0, NULL, &outputUIDSize, &outputUID);
            if((os_status == noErr) && outputUID) {
                outputDeviceUID = CFStringGetCStringPtr(outputUID, kCFStringEncodingUTF8);
            }
            else {
                outputUID = NULL;
            }
        }
        if (outputUID && !outputDeviceUID
         && ((outputDeviceUIDSize = CFStringGetMaximumSizeForEncoding(CFStringGetLength(outputUID), kCFStringEncodingUTF8) + 1) != 0)
         && ((outputDeviceUIDBuf = (char *)(alloca(outputDeviceUIDSize))) != NULL)
         && CFStringGetCString(outputUID, outputDeviceUIDBuf, outputDeviceUIDSize, kCFStringEncodingUTF8)) {
            outputDeviceUID = outputDeviceUIDBuf;
        }
        if (outputDeviceUID && (outputDeviceUID[0] != '\0')) {
            phone_ios_ios_phone.ringtoneSound = [[NSSound alloc] initWithContentsOfURL:fileURL byReference:true];
            phone_ios_ios_phone.ringtoneSound.playbackDeviceIdentifier = [NSString stringWithCString:((device && (device[0] != '\0')) ? device : outputDeviceUID) encoding:NSUTF8StringEncoding];
            phone_ios_ios_phone.ringtoneSound.loops = !notification_tone;
            [phone_ios_ios_phone.ringtoneSound play];
        }
        else {
            AudioServicesCreateSystemSoundID((__bridge CFURLRef)fileURL, &ringtoneSoundID);
            if (!notification_tone) {
                os_status = AudioServicesAddSystemSoundCompletion(ringtoneSoundID, NULL, NULL, ringtonePlayoutComplete, NULL);
                if (os_status != noErr) {
                    NSLog(@"AudioServicesAddSystemSoundCompletion failed %d", (int) os_status);
                }
            }
            if (allow_vibrate) {
                AudioServicesPlayAlertSound(ringtoneSoundID);
            }
            else {
                AudioServicesPlaySystemSound(ringtoneSoundID);
            }
        }
        if (isEarphonePlugged() || isBluetoothActive()) {
            phone_ios_ios_phone.ringtonePlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:fileURL error:nil];
            phone_ios_ios_phone.ringtonePlayer.numberOfLoops = -1;
            [phone_ios_ios_phone.ringtonePlayer prepareToPlay];
            [phone_ios_ios_phone.ringtonePlayer setVolume:0.0];
            [phone_ios_ios_phone.ringtonePlayer setDelegate:phone_ios_ios_phone];
            [phone_ios_ios_phone.ringtonePlayer play];
        }
        if (outputUID) CFRelease(outputUID);
    }
    else {
        if (allow_vibrate) {
            AudioServicesPlayAlertSound(kSystemSoundID_UserPreferredAlert);
        }
    }
#endif
    return noErr;
}

OSStatus createRingtonePlayer(const char *device, const char *melody, bool allow_vibrate)
{
    return createRingtonePlayerEx(device, melody, allow_vibrate, false);
}

OSStatus createNotificationTonePlayer(const char *device, const char *melody, bool allow_vibrate)
{
    return createRingtonePlayerEx(device, melody, allow_vibrate, true);
}
