import AudioToolbox
import CoreAudio
import Foundation

func getAudioObjectID(_ selector: AudioObjectPropertySelector,
                      objectID: AudioObjectID,
                      scope: AudioObjectPropertyScope = kAudioObjectPropertyScopeGlobal,
                      element: AudioObjectPropertyElement = kAudioObjectPropertyElementMain) -> AudioObjectID? {
    var address = AudioObjectPropertyAddress(mSelector: selector, mScope: scope, mElement: element)
    var value = AudioObjectID(0)
    var size = UInt32(MemoryLayout<AudioObjectID>.size)
    let status = AudioObjectGetPropertyData(objectID, &address, 0, nil, &size, &value)
    return status == noErr ? value : nil
}

func getString(_ selector: AudioObjectPropertySelector, objectID: AudioObjectID) -> String {
    var address = AudioObjectPropertyAddress(
        mSelector: selector,
        mScope: kAudioObjectPropertyScopeGlobal,
        mElement: kAudioObjectPropertyElementMain
    )
    var value: CFString = "" as CFString
    var size = UInt32(MemoryLayout<CFString>.size)
    let status = AudioObjectGetPropertyData(objectID, &address, 0, nil, &size, &value)
    return status == noErr ? value as String : "<unknown>"
}

func getUInt32(_ selector: AudioObjectPropertySelector,
               objectID: AudioObjectID,
               scope: AudioObjectPropertyScope = kAudioObjectPropertyScopeGlobal) -> UInt32? {
    var address = AudioObjectPropertyAddress(
        mSelector: selector,
        mScope: scope,
        mElement: kAudioObjectPropertyElementMain
    )
    var value: UInt32 = 0
    var size = UInt32(MemoryLayout<UInt32>.size)
    let status = AudioObjectGetPropertyData(objectID, &address, 0, nil, &size, &value)
    return status == noErr ? value : nil
}

func getDouble(_ selector: AudioObjectPropertySelector, objectID: AudioObjectID) -> Double? {
    var address = AudioObjectPropertyAddress(
        mSelector: selector,
        mScope: kAudioObjectPropertyScopeGlobal,
        mElement: kAudioObjectPropertyElementMain
    )
    var value = Double(0)
    var size = UInt32(MemoryLayout<Double>.size)
    let status = AudioObjectGetPropertyData(objectID, &address, 0, nil, &size, &value)
    return status == noErr ? value : nil
}

func describe<T>(_ value: T?) -> String {
    guard let value else { return "<unknown>" }
    return String(describing: value)
}

func streamCount(_ objectID: AudioObjectID, scope: AudioObjectPropertyScope) -> Int {
    var address = AudioObjectPropertyAddress(
        mSelector: kAudioDevicePropertyStreams,
        mScope: scope,
        mElement: kAudioObjectPropertyElementMain
    )
    var size: UInt32 = 0
    let sizeStatus = AudioObjectGetPropertyDataSize(objectID, &address, 0, nil, &size)
    guard sizeStatus == noErr else { return 0 }
    return Int(size) / MemoryLayout<AudioStreamID>.size
}

func allDevices() -> [AudioObjectID] {
    var address = AudioObjectPropertyAddress(
        mSelector: kAudioHardwarePropertyDevices,
        mScope: kAudioObjectPropertyScopeGlobal,
        mElement: kAudioObjectPropertyElementMain
    )
    var size: UInt32 = 0
    guard AudioObjectGetPropertyDataSize(AudioObjectID(kAudioObjectSystemObject), &address, 0, nil, &size) == noErr else {
        return []
    }
    let count = Int(size) / MemoryLayout<AudioObjectID>.size
    var devices = Array(repeating: AudioObjectID(0), count: count)
    guard AudioObjectGetPropertyData(AudioObjectID(kAudioObjectSystemObject), &address, 0, nil, &size, &devices) == noErr else {
        return []
    }
    return devices
}

let timestamp = ISO8601DateFormatter().string(from: Date())
print("timestamp_utc=\(timestamp)")

if let defaultInput = getAudioObjectID(kAudioHardwarePropertyDefaultInputDevice,
                                       objectID: AudioObjectID(kAudioObjectSystemObject)) {
    print("default_input_id=\(defaultInput)")
    print("default_input_name=\(getString(kAudioObjectPropertyName, objectID: defaultInput))")
    print("default_input_uid=\(getString(kAudioDevicePropertyDeviceUID, objectID: defaultInput))")
    print("default_input_transport=\(describe(getUInt32(kAudioDevicePropertyTransportType, objectID: defaultInput)))")
    print("default_input_nominal_sample_rate=\(describe(getDouble(kAudioDevicePropertyNominalSampleRate, objectID: defaultInput)))")
}

if let defaultOutput = getAudioObjectID(kAudioHardwarePropertyDefaultOutputDevice,
                                        objectID: AudioObjectID(kAudioObjectSystemObject)) {
    print("default_output_id=\(defaultOutput)")
    print("default_output_name=\(getString(kAudioObjectPropertyName, objectID: defaultOutput))")
}

print("devices:")
for device in allDevices().sorted() {
    let name = getString(kAudioObjectPropertyName, objectID: device)
    let uid = getString(kAudioDevicePropertyDeviceUID, objectID: device)
    let transport = describe(getUInt32(kAudioDevicePropertyTransportType, objectID: device))
    let rate = describe(getDouble(kAudioDevicePropertyNominalSampleRate, objectID: device))
    let inputStreams = streamCount(device, scope: kAudioDevicePropertyScopeInput)
    let outputStreams = streamCount(device, scope: kAudioDevicePropertyScopeOutput)
    print("- id=\(device) name=\"\(name)\" uid=\"\(uid)\" transport=\(transport) rate=\(rate) input_streams=\(inputStreams) output_streams=\(outputStreams)")
}
