import Foundation
import SwiftSignalKit
import AVFoundation
import UIKit

private var managedAudioSessionLogger: (String) -> Void = { _ in }

public func setManagedAudioSessionLogger(_ f: @escaping (String) -> Void) {
    managedAudioSessionLogger = f
}

func managedAudioSessionLog(_ what: @autoclosure () -> String) {
    managedAudioSessionLogger(what())
}


public enum ManagedAudioSessionType: Equatable {
    case ambient
    case play
    case playWithPossiblePortOverride
    case record(speaker: Bool, withOthers: Bool)
    case voiceCall
    case videoCall
    case recordWithOthers
    
    var isPlay: Bool {
        switch self {
            case .play, .playWithPossiblePortOverride:
                return true
            default:
                return false
        }
    }
}

private func nativeCategoryForType(_ type: ManagedAudioSessionType, headphones: Bool, outputMode: AudioSessionOutputMode) -> AVAudioSession.Category {
    switch type {
    case .ambient:
        return .ambient
    case .play:
        return .playback
    case .record, .recordWithOthers, .voiceCall, .videoCall:
        return .playAndRecord
    case .playWithPossiblePortOverride:
        if headphones {
            return .playback
        } else {
            switch outputMode {
            case .custom(.speaker), .system:
                return .playAndRecord
            default:
                return .playback
            }
        }
    }
}

public enum AudioSessionPortType {
    case generic
    case bluetooth
    case wired
}

public struct AudioSessionPort: Equatable {
    fileprivate let uid: String
    public let name: String
    public let type: AudioSessionPortType
}

public enum AudioSessionOutput: Equatable {
    case builtin
    case speaker
    case headphones
    case port(AudioSessionPort)
}

private let bluetoothPortTypes = Set<AVAudioSession.Port>([.bluetoothA2DP, .bluetoothLE, .bluetoothHFP])

private extension AudioSessionOutput {
    init(description: AVAudioSessionPortDescription) {
        var type: AudioSessionPortType = .generic
        if bluetoothPortTypes.contains(description.portType) {
            type = .bluetooth
        } else if description.uid == "Wired Headphones" || description.uid == "Wired Microphone" {
            type = .wired
        }
        
        self = .port(AudioSessionPort(uid: description.uid, name: description.portName, type: type))
    }
}

public enum AudioSessionOutputMode: Equatable {
    case system
    case speakerIfNoHeadphones
    case custom(AudioSessionOutput)
    
    public static func ==(lhs: AudioSessionOutputMode, rhs: AudioSessionOutputMode) -> Bool {
        switch lhs {
            case .system:
                if case .system = rhs {
                    return true
                } else {
                    return false
                }
            case .speakerIfNoHeadphones:
                if case .speakerIfNoHeadphones = rhs {
                    return true
                } else {
                    return false
                }
            case let .custom(output):
                if case .custom(output) = rhs {
                    return true
                } else {
                    return false
                }
        }
    }
}

private final class HolderRecord {
    let id: Int32
    let audioSessionType: ManagedAudioSessionType
    let control: ManagedAudioSessionControl
    let activate: (ManagedAudioSessionControl) -> Void
    let deactivate: (Bool) -> Signal<Void, NoError>
    let headsetConnectionStatusChanged: (Bool) -> Void
    let availableOutputsChanged: ([AudioSessionOutput], AudioSessionOutput?) -> Void
    let once: Bool
    var outputMode: AudioSessionOutputMode
    var active: Bool = false
    var deactivatingDisposable: Disposable? = nil
    
    init(id: Int32, audioSessionType: ManagedAudioSessionType, control: ManagedAudioSessionControl, activate: @escaping (ManagedAudioSessionControl) -> Void, deactivate: @escaping (Bool) -> Signal<Void, NoError>, headsetConnectionStatusChanged: @escaping (Bool) -> Void, availableOutputsChanged: @escaping ([AudioSessionOutput], AudioSessionOutput?) -> Void, once: Bool, outputMode: AudioSessionOutputMode) {
        self.id = id
        self.audioSessionType = audioSessionType
        self.control = control
        self.activate = activate
        self.deactivate = deactivate
        self.headsetConnectionStatusChanged = headsetConnectionStatusChanged
        self.availableOutputsChanged = availableOutputsChanged
        self.once = once
        self.outputMode = outputMode
    }
}

private final class ManagedAudioSessionControlActivate {
    let f: (AudioSessionActivationState) -> Void
    
    init(_ f: @escaping (AudioSessionActivationState) -> Void) {
        self.f = f
    }
}

public struct AudioSessionActivationState {
    public let isHeadsetConnected: Bool
}

public class ManagedAudioSessionControl {
    private let setupImpl: (Bool) -> Void
    private let activateImpl: (ManagedAudioSessionControlActivate) -> Void
    private let setupAndActivateImpl: (Bool, ManagedAudioSessionControlActivate) -> Void
    private let setOutputModeImpl: (AudioSessionOutputMode) -> Void
    
    fileprivate init(setupImpl: @escaping (Bool) -> Void, activateImpl: @escaping (ManagedAudioSessionControlActivate) -> Void, setOutputModeImpl: @escaping (AudioSessionOutputMode) -> Void, setupAndActivateImpl: @escaping (Bool, ManagedAudioSessionControlActivate) -> Void) {
        self.setupImpl = setupImpl
        self.activateImpl = activateImpl
        self.setOutputModeImpl = setOutputModeImpl
        self.setupAndActivateImpl = setupAndActivateImpl
    }
    
    public func setup(synchronous: Bool = false) {
        self.setupImpl(synchronous)
    }
    
    public func activate(_ completion: @escaping (AudioSessionActivationState) -> Void) {
        self.activateImpl(ManagedAudioSessionControlActivate(completion))
    }
    
    public func setupAndActivate(synchronous: Bool = false, _ completion: @escaping (AudioSessionActivationState) -> Void) {
        self.setupAndActivateImpl(synchronous, ManagedAudioSessionControlActivate(completion))
    }
    
    public func setOutputMode(_ mode: AudioSessionOutputMode) {
        self.setOutputModeImpl(mode)
    }
}

public final class ManagedAudioSession {
    public private(set) static var shared: ManagedAudioSession?
    
    private var nextId: Int32 = 0
    private let queue: Queue
    private let hasLoudspeaker: Bool
    private var holders: [HolderRecord] = []
    private var currentTypeAndOutputMode: (ManagedAudioSessionType, AudioSessionOutputMode)?
    private var deactivateTimer: SwiftSignalKit.Timer?
    
    private let isHeadsetPluggedInSync = Atomic<Bool>(value: false)
    private var isHeadsetPluggedInValue = false {
        didSet {
            if self.isHeadsetPluggedInValue != oldValue {
                let _ = self.isHeadsetPluggedInSync.swap(self.isHeadsetPluggedInValue)
            }
        }
    }
    
    public func getIsHeadsetPluggedIn() -> Bool {
        return self.isHeadsetPluggedInSync.with { $0 }
    }
    
    private let outputsToHeadphonesSubscribers = Bag<(Bool) -> Void>()
    
    private var availableOutputsValue: [AudioSessionOutput] = []
    private var currentOutputValue: AudioSessionOutput?
    
    private let isActiveSubscribers = Bag<(Bool) -> Void>()
    private let isPlaybackActiveSubscribers = Bag<(Bool) -> Void>()
    
    private var isActiveValue: Bool = false
    private var callKitAudioSessionIsActive: Bool = false
    
    public init() {
        self.queue = Queue()
        
        self.hasLoudspeaker = UIDevice.current.model == "iPhone"
        
        let queue = self.queue
        NotificationCenter.default.addObserver(forName: AVAudioSession.routeChangeNotification, object: AVAudioSession.sharedInstance(), queue: nil, using: { [weak self] _ in
            queue.async {
                self?.updateCurrentAudioRouteInfo()
            }
        })
        
        NotificationCenter.default.addObserver(forName: AVAudioSession.interruptionNotification, object: AVAudioSession.sharedInstance(), queue: nil, using: { [weak self] notification in
            managedAudioSessionLog("Interruption received")

            guard let info = notification.userInfo,
                let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
                let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
                    return
            }

            managedAudioSessionLog("Interruption type: \(type)")
            
            queue.async {
                if let strongSelf = self {
                    if type == .began {
                        strongSelf.updateHolders(interruption: true)
                    }
                }
            }
        })

        NotificationCenter.default.addObserver(forName: AVAudioSession.mediaServicesWereLostNotification, object: AVAudioSession.sharedInstance(), queue: nil, using: { [weak self] _ in
            managedAudioSessionLog("Media Services were lost")
            queue.after(1.0, {
                if let strongSelf = self {
                    if let (type, outputMode) = strongSelf.currentTypeAndOutputMode {
                        strongSelf.setup(type: type, outputMode: outputMode, activateNow: true)
                    }
                }
            })
        })
        
        queue.async {
            self.isHeadsetPluggedInValue = self.isHeadsetPluggedIn()
            self.updateCurrentAudioRouteInfo()
        }
        
        ManagedAudioSession.shared = self
    }
    
    deinit {
        self.deactivateTimer?.invalidate()
    }
    
    private func updateCurrentAudioRouteInfo() {
        let value = self.isHeadsetPluggedIn()
        if self.isHeadsetPluggedInValue != value {
            self.isHeadsetPluggedInValue = value
            if let (_, outputMode) = self.currentTypeAndOutputMode {
                if case .speakerIfNoHeadphones = outputMode {
                    self.updateOutputMode(outputMode)
                }
            }
            for subscriber in self.outputsToHeadphonesSubscribers.copyItems() {
                subscriber(value)
            }
            for i in 0 ..< self.holders.count {
                if self.holders[i].active {
                    self.holders[i].headsetConnectionStatusChanged(value)
                    break
                }
            }
        }
        
        let audioSession = AVAudioSession.sharedInstance()
        
        var availableOutputs: [AudioSessionOutput] = []
        var activeOutput: AudioSessionOutput = .builtin
        
        if let availableInputs = audioSession.availableInputs {
            var hasHeadphones = false
            var hasBluetoothHeadphones = false
            
            var headphonesAreActive = false
            loop: for currentOutput in audioSession.currentRoute.outputs {
                switch currentOutput.portType {
                case .headphones, .bluetoothA2DP, .bluetoothHFP:
                    headphonesAreActive = true
                    hasHeadphones = true
                    hasBluetoothHeadphones = [.bluetoothA2DP, .bluetoothHFP].contains(currentOutput.portType)
                    activeOutput = .headphones
                    break loop
                default:
                    break
                }
            }
            
            for input in availableInputs {
                var isActive = false
                for currentInput in audioSession.currentRoute.inputs {
                    if currentInput.uid == input.uid {
                        isActive = true
                    }
                }
                
                if input.portType == .builtInMic {
                    if isActive && !headphonesAreActive {
                        activeOutput = .builtin
                        inner: for currentOutput in audioSession.currentRoute.outputs {
                            if currentOutput.portType == .builtInSpeaker {
                                activeOutput = .speaker
                                break inner
                            }
                        }
                    }
                    continue
                }
                if input.portType == .headphones {
                    if isActive {
                        activeOutput = .headphones
                    }
                    hasHeadphones = true
                    continue
                }
                let output = AudioSessionOutput(description: input)
                availableOutputs.append(output)
                if isActive {
                    activeOutput = output
                }
            }
            
            if self.hasLoudspeaker {
                availableOutputs.insert(.speaker, at: 0)
            }
            
            if hasHeadphones && !hasBluetoothHeadphones {
                availableOutputs.insert(.headphones, at: 0)
            }
            availableOutputs.insert(.builtin, at: 0)
        }
        
        if self.availableOutputsValue != availableOutputs || self.currentOutputValue != activeOutput {
            self.availableOutputsValue = availableOutputs
            self.currentOutputValue = activeOutput
            for i in 0 ..< self.holders.count {
                if self.holders[i].active {
                    self.holders[i].availableOutputsChanged(availableOutputs, activeOutput)
                    break
                }
            }
        }
    }
    
    public func headsetConnected() -> Signal<Bool, NoError> {
        let queue = self.queue
        return Signal { [weak self] subscriber in
            if let strongSelf = self {
                subscriber.putNext(strongSelf.isHeadsetPluggedInValue)
                
                let index = strongSelf.outputsToHeadphonesSubscribers.add({ value in
                    subscriber.putNext(value)
                })
                
                return ActionDisposable {
                    queue.async {
                        if let strongSelf = self {
                            strongSelf.outputsToHeadphonesSubscribers.remove(index)
                        }
                    }
                }
            } else {
                return EmptyDisposable
            }
        } |> runOn(queue)
    }
    
    public func isActive() -> Signal<Bool, NoError> {
        let queue = self.queue
        return Signal { [weak self] subscriber in
            if let strongSelf = self {
                subscriber.putNext(strongSelf.isActiveValue || strongSelf.callKitAudioSessionIsActive)
                
                let index = strongSelf.isActiveSubscribers.add({ value in
                    subscriber.putNext(value)
                })
                
                return ActionDisposable {
                    queue.async {
                        if let strongSelf = self {
                            strongSelf.isActiveSubscribers.remove(index)
                        }
                    }
                }
            } else {
                return EmptyDisposable
            }
        } |> runOn(queue)
    }
    
    public func isPlaybackActive() -> Signal<Bool, NoError> {
        let queue = self.queue
        return Signal { [weak self] subscriber in
            if let strongSelf = self {
                subscriber.putNext(strongSelf.currentTypeAndOutputMode?.0.isPlay ?? false)
                
                let index = strongSelf.isPlaybackActiveSubscribers.add({ value in
                    subscriber.putNext(value)
                })
                
                return ActionDisposable {
                    queue.async {
                        if let strongSelf = self {
                            strongSelf.isPlaybackActiveSubscribers.remove(index)
                        }
                    }
                }
            } else {
                return EmptyDisposable
            }
        } |> runOn(queue)
    }
    
    public func isOtherAudioPlaying() -> Bool {
        return AVAudioSession.sharedInstance().secondaryAudioShouldBeSilencedHint
    }
    
    public func push(audioSessionType: ManagedAudioSessionType, outputMode: AudioSessionOutputMode = .system, once: Bool = false, activate: @escaping (AudioSessionActivationState) -> Void, deactivate: @escaping (Bool) -> Signal<Void, NoError>) -> Disposable {
        return self.push(audioSessionType: audioSessionType, once: once, manualActivate: { control in
            control.setupAndActivate(synchronous: false, { state in
                activate(state)
            })
        }, deactivate: deactivate)
    }
    
    public func push(audioSessionType: ManagedAudioSessionType, outputMode: AudioSessionOutputMode = .system, once: Bool = false, activateImmediately: Bool = false, manualActivate: @escaping (ManagedAudioSessionControl) -> Void, deactivate: @escaping (Bool) -> Signal<Void, NoError>, headsetConnectionStatusChanged: @escaping (Bool) -> Void = { _ in }, availableOutputsChanged: @escaping ([AudioSessionOutput], AudioSessionOutput?) -> Void = { _, _ in }) -> Disposable {
        let id = OSAtomicIncrement32(&self.nextId)
        let queue = self.queue
        queue.async {
            self.holders.append(HolderRecord(id: id, audioSessionType: audioSessionType, control: ManagedAudioSessionControl(setupImpl: { [weak self] synchronous in
                let f: () -> Void = {
                    if let strongSelf = self {
                        for holder in strongSelf.holders {
                            if holder.id == id && holder.active {
                                strongSelf.setup(type: audioSessionType, outputMode: holder.outputMode, activateNow: activateImmediately)
                                break
                            }
                        }
                    }
                }
                
                if synchronous {
                    queue.sync(f)
                } else {
                    queue.async(f)
                }
            }, activateImpl: { [weak self] completion in
                if let strongSelf = self {
                    strongSelf.queue.async {
                        for holder in strongSelf.holders {
                            if holder.id == id && holder.active {
                                if strongSelf.currentTypeAndOutputMode?.0 != holder.audioSessionType || strongSelf.currentTypeAndOutputMode?.1 != holder.outputMode {
                                    strongSelf.setup(type: holder.audioSessionType, outputMode: holder.outputMode, activateNow: true)
                                } else {
                                    strongSelf.activate()
                                }
                                completion.f(AudioSessionActivationState(isHeadsetConnected: strongSelf.isHeadsetPluggedInValue))
                                break
                            }
                        }
                    }
                }
            }, setOutputModeImpl: { [weak self] value in
                if let strongSelf = self {
                    strongSelf.queue.async {
                        for holder in strongSelf.holders {
                            if holder.id == id {
                                if holder.outputMode != value {
                                    holder.outputMode = value
                                }
                                
                                if holder.active {
                                    strongSelf.updateOutputMode(value)
                                }
                            }
                        }
                    }
                }
            }, setupAndActivateImpl: { [weak self] synchronous, completion in
                queue.async {
                    let f: () -> Void = {
                        if let strongSelf = self {
                            for holder in strongSelf.holders {
                                if holder.id == id && holder.active {
                                    strongSelf.setup(type: audioSessionType, outputMode: holder.outputMode, activateNow: true)
                                    completion.f(AudioSessionActivationState(isHeadsetConnected: strongSelf.isHeadsetPluggedInValue))
                                    break
                                }
                            }
                        }
                    }
                    
                    if synchronous {
                        queue.sync(f)
                    } else {
                        queue.async(f)
                    }
                }
            }), activate: { [weak self] state in
                manualActivate(state)
                queue.async {
                    if let strongSelf = self {
                        strongSelf.updateCurrentAudioRouteInfo()
                        availableOutputsChanged(strongSelf.availableOutputsValue, strongSelf.currentOutputValue)
                    }
                }
            }, deactivate: deactivate, headsetConnectionStatusChanged: headsetConnectionStatusChanged, availableOutputsChanged: availableOutputsChanged, once: once, outputMode: outputMode))
            self.updateHolders()
        }
        return ActionDisposable { [weak self] in
            if let strongSelf = self {
                strongSelf.queue.async {
                    strongSelf.removeDeactivatedHolder(id: id)
                }
            }
        }
    }

    public func dropAll() {
        self.queue.async {
            self.updateHolders(interruption: true)
        }
    }
    
    private func removeDeactivatedHolder(id: Int32) {
        assert(self.queue.isCurrent())
        
        for i in 0 ..< self.holders.count {
            if self.holders[i].id == id {
                self.holders[i].deactivatingDisposable?.dispose()
                self.holders.remove(at: i)
                self.updateHolders()
                break
            }
        }
    }
    
    private func updateHolders(interruption: Bool = false) {
        assert(self.queue.isCurrent())
        
        managedAudioSessionLog("holder count \(self.holders.count)")
        
        if !self.holders.isEmpty {
            var activeIndex: Int?
            var deactivating = false
            var index = 0
            for record in self.holders {
                if record.active {
                    activeIndex = index
                    break
                }
                else if record.deactivatingDisposable != nil {
                    deactivating = true
                }
                index += 1
            }
            
            var lastIsRecordWithOthers = false
            if let lastHolder = self.holders.last {
                if case let .record(_, withOthers) = lastHolder.audioSessionType {
                    lastIsRecordWithOthers = withOthers
                } else if case .recordWithOthers = lastHolder.audioSessionType {
                    lastIsRecordWithOthers = true
                }
            }
            if !deactivating {
                if let activeIndex = activeIndex {
                    var deactivate = false
                    var temporary = false
                    
                    if interruption {
                        if self.holders[activeIndex].audioSessionType != .voiceCall {
                            deactivate = true
                        }
                    } else {
                        if activeIndex != self.holders.count - 1 {
                            if lastIsRecordWithOthers {
                                deactivate = true
                                temporary = true
                            } else if self.holders[activeIndex].audioSessionType == .voiceCall {
                                deactivate = false
                            } else {
                                deactivate = true
                            }
                        }
                    }
                    
                    if deactivate {
                        self.holders[activeIndex].active = false
                        let id = self.holders[activeIndex].id
                        self.holders[activeIndex].deactivatingDisposable = (self.holders[activeIndex].deactivate(temporary)
                        |> deliverOn(self.queue)).start(completed: { [weak self] in
                            guard let strongSelf = self else {
                                return
                            }
                            var index = 0
                            for currentRecord in strongSelf.holders {
                                if currentRecord.id == id {
                                    currentRecord.deactivatingDisposable = nil
                                    if currentRecord.once {
                                        strongSelf.holders.remove(at: index)
                                    }
                                    break
                                }
                                index += 1
                            }
                            strongSelf.updateHolders()
                        })
                    }
                } else if activeIndex == nil {
                    let lastIndex = self.holders.count - 1
                    
                    self.deactivateTimer?.invalidate()
                    self.deactivateTimer = nil
                    
                    self.holders[lastIndex].active = true
                    self.holders[lastIndex].activate(self.holders[lastIndex].control)
                }
            }
        } else {
            self.applyNoneDelayed()
        }
    }
    
    private func applyNoneDelayed() {
        self.deactivateTimer?.invalidate()
        
        var immediately = false
        if let mode = self.currentTypeAndOutputMode?.0 {
            switch mode {
                case .voiceCall, .record:
                    immediately = true
                default:
                    break
            }
        }
        
        if immediately {
            self.applyNone()
        } else {
            let deactivateTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: false, completion: { [weak self] in
                if let strongSelf = self {
                    strongSelf.applyNone()
                }
            }, queue: self.queue)
            self.deactivateTimer = deactivateTimer
            deactivateTimer.start()
        }
    }
    
    private func isHeadsetPluggedIn() -> Bool {
        assert(self.queue.isCurrent())
        
        let route = AVAudioSession.sharedInstance().currentRoute
        //managedAudioSessionLog("\(route)")
        for desc in route.outputs {
            if desc.portType == .headphones || desc.portType == .bluetoothA2DP || desc.portType == .bluetoothHFP {
                return true
            }
        }
        
        return false
    }
    
    private func applyNone() {
        self.deactivateTimer?.invalidate()
        self.deactivateTimer = nil
        
        let wasPlaybackActive = self.currentTypeAndOutputMode?.0.isPlay ?? false
        self.currentTypeAndOutputMode = nil
        
        managedAudioSessionLog("ManagedAudioSession setting active false")
        do {
            try AVAudioSession.sharedInstance().setActive(false, options: [.notifyOthersOnDeactivation])
            try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
            try AVAudioSession.sharedInstance().setPreferredInput(nil)
        } catch let error {
            managedAudioSessionLog("ManagedAudioSession applyNone error \(error), waiting")

            Thread.sleep(forTimeInterval: 2.0)

            do {
                try AVAudioSession.sharedInstance().setActive(false, options: [.notifyOthersOnDeactivation])
                try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
                try AVAudioSession.sharedInstance().setPreferredInput(nil)
            } catch let error {
                managedAudioSessionLog("ManagedAudioSession applyNone repeated error \(error), giving up")
            }
        }
        
        self.isActiveValue = false
        for subscriber in self.isActiveSubscribers.copyItems() {
            subscriber(self.isActiveValue || self.callKitAudioSessionIsActive)
        }
        if wasPlaybackActive {
            for subscriber in self.isPlaybackActiveSubscribers.copyItems() {
                subscriber(false)
            }
        }
    }
    
    private func setup(type: ManagedAudioSessionType, outputMode: AudioSessionOutputMode, activateNow: Bool) {
        self.deactivateTimer?.invalidate()
        self.deactivateTimer = nil
        
        let wasPlaybackActive = self.currentTypeAndOutputMode?.0.isPlay ?? false
        
        if self.currentTypeAndOutputMode == nil || self.currentTypeAndOutputMode! != (type, outputMode) {
            self.currentTypeAndOutputMode = (type, outputMode)
            
            do {
                let nativeCategory = nativeCategoryForType(type, headphones: self.isHeadsetPluggedInValue, outputMode: outputMode)
                
                managedAudioSessionLog("ManagedAudioSession setting category for \(type) (native: \(nativeCategory)) activateNow: \(activateNow)")
                var options: AVAudioSession.CategoryOptions = []
                switch type {
                    case .play, .ambient:
                        break
                    case .playWithPossiblePortOverride:
                        if case .playAndRecord = nativeCategory {
                            options.insert(.allowBluetoothA2DP)
                        }
                    case .voiceCall, .videoCall:
                        options.insert(.allowBluetooth)
                        options.insert(.allowBluetoothA2DP)
                        options.insert(.mixWithOthers)
                    case .record, .recordWithOthers:
                        options.insert(.allowBluetooth)
                }
                managedAudioSessionLog("ManagedAudioSession setting category and options")
                let mode: AVAudioSession.Mode
                switch type {
                    case .voiceCall:
                        mode = .voiceChat
                        options.insert(.mixWithOthers)
                    case .videoCall:
                        mode = .videoChat
                        options.insert(.mixWithOthers)
                    case .recordWithOthers:
                        mode = .videoRecording
                        options.insert(.mixWithOthers)
                    default:
                        mode = .default
                }
                try AVAudioSession.sharedInstance().setCategory(nativeCategory, options: options)
                try AVAudioSession.sharedInstance().setMode(mode)
                if AVAudioSession.sharedInstance().categoryOptions != options {
                    managedAudioSessionLog("ManagedAudioSession resetting options")
                    try AVAudioSession.sharedInstance().setCategory(nativeCategory, options: options)
                }
                /*if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
                    try AVAudioSession.sharedInstance().setCategory(nativeCategory, mode: mode, policy: .default, options: options)
                } else {
                    AVAudioSession.sharedInstance().perform(NSSelectorFromString("setCategory:error:"), with: nativeCategory)
                    try AVAudioSession.sharedInstance().setMode(mode)
                }*/
            } catch let error {
                managedAudioSessionLog("ManagedAudioSession setup error \(error)")
            }
        }
        
        self.isActiveValue = true
        for subscriber in self.isActiveSubscribers.copyItems() {
            subscriber(self.isActiveValue || self.callKitAudioSessionIsActive)
        }
        if !wasPlaybackActive && (self.currentTypeAndOutputMode?.0.isPlay ?? false) {
            for subscriber in self.isPlaybackActiveSubscribers.copyItems() {
                subscriber(true)
            }
        }
        
        if activateNow {
            self.activate()
        }
    }
    
    public func applyVoiceChatOutputModeInCurrentAudioSession(outputMode: AudioSessionOutputMode) {
        managedAudioSessionLog("applyVoiceChatOutputModeInCurrentAudioSession \(outputMode)")
        
        do {
            var resetToBuiltin = false
            switch outputMode {
            case .system:
                resetToBuiltin = true
            case let .custom(output):
                switch output {
                case .builtin:
                    resetToBuiltin = true
                case .speaker:
                    if let routes = AVAudioSession.sharedInstance().availableInputs {
                        for route in routes {
                            if route.portType == .builtInMic {
                                let _ = try? AVAudioSession.sharedInstance().setPreferredInput(route)
                                break
                            }
                        }
                    }
                    try AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker)
                case .headphones:
                    break
                case let .port(port):
                    try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
                    if let routes = AVAudioSession.sharedInstance().availableInputs {
                        for route in routes {
                            if route.uid == port.uid {
                                let _ = try? AVAudioSession.sharedInstance().setPreferredInput(route)
                                break
                            }
                        }
                    }
                }
            case .speakerIfNoHeadphones:
                try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
            }
            
            if resetToBuiltin {
                try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
                if let routes = AVAudioSession.sharedInstance().availableInputs {
                    for route in routes {
                        if route.portType == .builtInMic {
                            let _ = try? AVAudioSession.sharedInstance().setPreferredInput(route)
                            break
                        }
                    }
                }
            }
        } catch let e {
            managedAudioSessionLog("applyVoiceChatOutputModeInCurrentAudioSession error: \(e)")
        }
    }
    
    private func setupOutputMode(_ outputMode: AudioSessionOutputMode, type: ManagedAudioSessionType) throws {
        managedAudioSessionLog("ManagedAudioSession setup \(outputMode) for \(type)")
        var resetToBuiltin = false
        switch outputMode {
        case .system:
            resetToBuiltin = true
        case let .custom(output):
            switch output {
                case .builtin:
                    resetToBuiltin = true
                case .speaker:
                    if type == .voiceCall {
                        if let routes = AVAudioSession.sharedInstance().availableInputs {
                            for route in routes {
                                if route.portType == .builtInMic {
                                    let _ = try?  AVAudioSession.sharedInstance().setInputDataSource(route.selectedDataSource)
                                    let _ = try? AVAudioSession.sharedInstance().setPreferredInput(route)
                                    break
                                }
                            }
                        }
                    }
                    try AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker)
                case .headphones:
                    break
                case let .port(port):
                    try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
                    if let routes = AVAudioSession.sharedInstance().availableInputs {
                        for route in routes {
                            if route.uid == port.uid {
                                let _ = try? AVAudioSession.sharedInstance().setPreferredInput(route)
                                break
                            }
                        }
                    }
            }
        case .speakerIfNoHeadphones:
            try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
        }
        
        if resetToBuiltin {
            var updatedType = type
            if case .record(false, let withOthers) = updatedType, self.isHeadsetPluggedInValue {
                updatedType = .record(speaker: true, withOthers: withOthers)
            }
            switch updatedType {
                case .record(false, _):
                    try AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker)
                case .voiceCall, .playWithPossiblePortOverride, .record(true, _):
                    try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
                    if let routes = AVAudioSession.sharedInstance().availableInputs {
                        var alreadySet = false
                        if self.isHeadsetPluggedInValue {
                            if case .voiceCall = updatedType, case .custom(.builtin) = outputMode {
                            } else {
                                loop: for route in routes {
                                    switch route.portType {
                                    case .headphones, .bluetoothA2DP, .bluetoothHFP:
                                        let _ = try? AVAudioSession.sharedInstance().setPreferredInput(route)
                                        alreadySet = true
                                        break loop
                                    default:
                                        break
                                    }
                                }
                            }
                        }
                        if !alreadySet {
                            for route in routes {
                                if route.portType == .builtInMic {
                                    if case .record = updatedType, self.isHeadsetPluggedInValue {
                                    } else {
                                        //let _ = try? AVAudioSession.sharedInstance().setPreferredInput(route)
                                        let _ = try? AVAudioSession.sharedInstance().setInputDataSource(nil)
                                    }
                                    break
                                }
                            }
                        }
                    }
                default:
                    try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
            }
        }
    }
    
    private func activate() {
        if let (type, outputMode) = self.currentTypeAndOutputMode {
            do {
                let startTime = CFAbsoluteTimeGetCurrent()
                
                try AVAudioSession.sharedInstance().setActive(true, options: [.notifyOthersOnDeactivation])
                
                managedAudioSessionLog("\(CFAbsoluteTimeGetCurrent()) AudioSession activate: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms")
                
                try self.setupOutputMode(outputMode, type: type)
                
                managedAudioSessionLog("\(CFAbsoluteTimeGetCurrent()) AudioSession setupOutputMode: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms")
                
                self.updateCurrentAudioRouteInfo()
                
                managedAudioSessionLog("\(CFAbsoluteTimeGetCurrent()) AudioSession updateCurrentAudioRouteInfo: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms")
                
                if case .voiceCall = type {
                    //try AVAudioSession.sharedInstance().setPreferredIOBufferDuration(0.005)
                }
            } catch let error {
                managedAudioSessionLog("ManagedAudioSession activate error \(error)")
            }
        }
    }
    
    private func updateOutputMode(_ outputMode: AudioSessionOutputMode) {
        if let (type, _) = self.currentTypeAndOutputMode {
            self.setup(type: type, outputMode: outputMode, activateNow: true)
        }
    }
    
    public func callKitActivatedAudioSession() {
        self.queue.async {
            managedAudioSessionLog("ManagedAudioSession callKitActivatedAudioSession")
            self.callKitAudioSessionIsActive = true
            self.updateHolders()
            
            for subscriber in self.isActiveSubscribers.copyItems() {
                subscriber(self.isActiveValue || self.callKitAudioSessionIsActive)
            }
        }
    }
    
    public func callKitDeactivatedAudioSession() {
        self.queue.async {
            managedAudioSessionLog("ManagedAudioSession callKitDeactivatedAudioSession")
            self.callKitAudioSessionIsActive = false
            self.updateHolders()
            
            for subscriber in self.isActiveSubscribers.copyItems() {
                subscriber(self.isActiveValue || self.callKitAudioSessionIsActive)
            }
        }
    }
}