/* -*- c++ -*- */
/*
 * Copyright 2014 Free Software Foundation, Inc.
 *
 * This file is part of GNU Radio.
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 *
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "../audio_registry.h"

#include <gnuradio/audio/osx_impl.h>
#include <gnuradio/logger.h>

#include <algorithm>
#include <locale>
#include <memory>
#include <stdexcept>

std::ostream& operator<<(std::ostream& s, const AudioStreamBasicDescription& asbd)
{
    char format_id[sizeof(asbd.mFormatID) + 1];
    memcpy(format_id, (void*)(&asbd.mFormatID), sizeof(asbd.mFormatID));
    format_id[sizeof(asbd.mFormatID)] = 0;
    s << "  Sample Rate      : " << asbd.mSampleRate << std::endl;
    s << "  Format ID        : " << format_id << std::endl;
    s << "  Format Flags     : " << asbd.mFormatFlags << std::endl;
    s << "    " << ((asbd.mFormatFlags & kAudioFormatFlagIsFloat) != 0) << " : Is Float"
      << std::endl;
    s << "    " << ((asbd.mFormatFlags & kAudioFormatFlagIsBigEndian) != 0)
      << " : Is Big Endian" << std::endl;
    s << "    " << ((asbd.mFormatFlags & kAudioFormatFlagIsSignedInteger) != 0)
      << " : Is Signed Integer" << std::endl;
    s << "    " << ((asbd.mFormatFlags & kAudioFormatFlagIsPacked) != 0) << " : Is Packed"
      << std::endl;
    s << "    " << ((asbd.mFormatFlags & kAudioFormatFlagIsAlignedHigh) != 0)
      << " : Is Aligned High" << std::endl;
    s << "    " << ((asbd.mFormatFlags & kAudioFormatFlagIsNonInterleaved) != 0)
      << " : Is Non-Interleaved" << std::endl;
    s << "    " << ((asbd.mFormatFlags & kAudioFormatFlagIsNonMixable) != 0)
      << " : Is Non-Mixable" << std::endl;
    s << "  Bytes / Packet   : " << asbd.mBytesPerPacket << std::endl;
    s << "  Frames / Packet  : " << asbd.mFramesPerPacket << std::endl;
    s << "  Bytes / Frame    : " << asbd.mBytesPerFrame << std::endl;
    s << "  Channels / Frame : " << asbd.mChannelsPerFrame << std::endl;
    s << "  Bits / Channel   : " << asbd.mBitsPerChannel;
    return (s);
};

namespace gr {
namespace audio {
namespace osx {

static UInt32 _get_num_channels(AudioDeviceID ad_id, AudioObjectPropertyScope scope)
{
    // retrieve the AudioBufferList associated with this ID using
    // the provided scope

    UInt32 num_channels = 0;
    UInt32 prop_size = 0;
    AudioObjectPropertyAddress ao_address = { kAudioDevicePropertyStreamConfiguration,
                                              scope,
                                              0 };
    OSStatus err = noErr;
    if ((err = AudioObjectGetPropertyDataSize(ad_id, &ao_address, 0, NULL, &prop_size)) ==
        noErr) {
        std::unique_ptr<AudioBufferList[]> buf_list(
            reinterpret_cast<AudioBufferList*>(new char[prop_size]));
        if ((err = AudioObjectGetPropertyData(
                 ad_id, &ao_address, 0, NULL, &prop_size, buf_list.get())) == noErr) {
            for (UInt32 mm = 0; mm < buf_list.get()->mNumberBuffers; ++mm) {
                num_channels += buf_list.get()->mBuffers[mm].mNumberChannels;
            }
        } else {
            // assume 2 channels
            num_channels = 2;
        }
    } else {
        // assume 2 channels
        num_channels = 2;
    }
    return (num_channels);
}

// works with both char and wchar_t
template <typename charT>
struct ci_equal {
    ci_equal(const std::locale& loc) : loc_(loc) {}
    bool operator()(charT ch1, charT ch2)
    {
        return std::tolower(ch1, loc_) == std::tolower(ch2, loc_);
    }

private:
    const std::locale& loc_;
};

// find substring (case insensitive)
static std::string::size_type ci_find_substr(const std::string& str1,
                                             const std::string& str2,
                                             const std::locale& loc = std::locale())
{
    std::string::const_iterator it = std::search(str1.begin(),
                                                 str1.end(),
                                                 str2.begin(),
                                                 str2.end(),
                                                 ci_equal<std::string::value_type>(loc));
    if (it != str1.end()) {
        return (it - str1.begin());
    }
    // not found
    return (std::string::npos);
}

void get_num_channels_for_audio_device_id(AudioDeviceID ad_id,
                                          UInt32* n_input,
                                          UInt32* n_output)
{
    if (n_input) {
        *n_input = _get_num_channels(ad_id, kAudioDevicePropertyScopeInput);
    }
    if (n_output) {
        *n_output = _get_num_channels(ad_id, kAudioDevicePropertyScopeOutput);
    }
}

void find_audio_devices(const std::string& device_name,
                        bool is_input,
                        std::vector<AudioDeviceID>* all_ad_ids,
                        std::vector<std::string>* all_names)
{
    if ((!all_ad_ids) && (!all_names)) {
        // if nothing is requested, no point in doing anything!
        return;
    }

    OSStatus err = noErr;

    // retrieve the size of the array of known audio device IDs

    UInt32 prop_size = 0;

    AudioObjectPropertyAddress ao_address = { kAudioHardwarePropertyDevices,
                                              kAudioObjectPropertyScopeGlobal,
                                              kAudioObjectPropertyElementMaster };

    if ((err = AudioObjectGetPropertyDataSize(
             kAudioObjectSystemObject, &ao_address, 0, NULL, &prop_size)) != noErr) {
#if _OSX_AU_DEBUG_
        gr::logger_ptr logger, debug_logger;
        gr::configure_default_loggers(
            logger, debug_logger, "osx_impl::find_audio_devices");
        logger->error("Unable to retrieve number of audio objects: {}", err);
#endif
        return;
    }

    // get the total number of audio devices (input and output)

    UInt32 num_devices = prop_size / sizeof(AudioDeviceID);

    // retrieve all audio device ids

    std::unique_ptr<AudioDeviceID[]> all_dev_ids(new AudioDeviceID[num_devices]);

    if ((err = AudioObjectGetPropertyData(kAudioObjectSystemObject,
                                          &ao_address,
                                          0,
                                          NULL,
                                          &prop_size,
                                          all_dev_ids.get())) != noErr) {
#if _OSX_AU_DEBUG_
        logger->error("Unable to retrieve audio object ids: {}", err);
#endif
        return;
    }

    // success; loop over all retrieved output device ids, retrieving
    // the name for each and comparing with the desired name.

    std::vector<std::string> valid_names(num_devices);
    std::vector<UInt32> valid_indices(num_devices);
    UInt32 num_found_devices = 0;
    AudioObjectPropertyScope scope =
        is_input ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput;

    for (UInt32 nn = 0; nn < num_devices; ++nn) {

        // make sure this device has input / output channels (it might
        // also have output / input channels, too, but we do not care
        // about that here)

        AudioDeviceID t_id = all_dev_ids[nn];

        if (is_input) {
            UInt32 n_input_channels = 0;
            get_num_channels_for_audio_device_id(t_id, &n_input_channels, NULL);
            if (n_input_channels == 0) {
                // no input channels; must be output device; just continue
                // to the next audio device.
                continue;
            }
        } else {
            UInt32 n_output_channels = 0;
            get_num_channels_for_audio_device_id(t_id, NULL, &n_output_channels);
            if (n_output_channels == 0) {
                // no output channels; must be input device; just continue
                // to the next audio device.
                continue;
            }
        }

        // retrieve the device name; max name length is 64 characters.

        prop_size = 65;
        char c_name_buf[prop_size];
        bzero((void*)c_name_buf, prop_size);
        --prop_size;

        AudioObjectPropertyAddress ao_address = { kAudioDevicePropertyDeviceName,
                                                  scope,
                                                  0 };

        if ((err = AudioObjectGetPropertyData(
                 t_id, &ao_address, 0, NULL, &prop_size, (void*)c_name_buf)) != noErr) {
#if _OSX_AU_DEBUG_
            logger->error("Unable to retrieve audio device name #{:d}: {}", nn + 1, err);
#endif
            continue;
        }
        std::string name_buf(c_name_buf);

        // compare the retrieved name with the desired one, if
        // provided; case insensitive.

        if (device_name.length() > 0) {

            std::string::size_type found = ci_find_substr(name_buf, device_name);
            if (found == std::string::npos) {
                // not found; continue to the next ID
                continue;
            }
        }

        // store this info

        valid_names[nn] = name_buf;
        valid_indices[num_found_devices++] = nn;
    }

    // resize valid function arguments, then copy found values

    if (all_ad_ids) {
        all_ad_ids->resize(num_found_devices);
        for (UInt32 nn = 0; nn < num_found_devices; ++nn) {
            (*all_ad_ids)[nn] = all_dev_ids[valid_indices[nn]];
        }
    }

    if (all_names) {
        all_names->resize(num_found_devices);
        for (UInt32 nn = 0; nn < num_found_devices; ++nn) {
            (*all_names)[nn] = valid_names[valid_indices[nn]];
        }
    }
}

} /* namespace osx */
} /* namespace audio */
} /* namespace gr */
