/*
    ==============================================================================

    InputDeviceSelector.cpp
    Created: 18 Aug 2024 8:20:19pm
    Author:  binya

    ==============================================================================
*/

#include "InputDeviceSelector.h"

ChannelSelectorListBox::ChannelSelectorListBox(juce::AudioDeviceManager& audioDeviceManager, int minNumInputChannels, int maxNumInputChannels, bool useStereoPairs)
    : manager(audioDeviceManager), minInputChannels(minNumInputChannels), maxInputChannels(maxNumInputChannels), useStereoPairs(useStereoPairs)
{
    setModel(this);
    audioDeviceManager.addChangeListener(this);

    changeListenerCallback(nullptr);
}

ChannelSelectorListBox::~ChannelSelectorListBox()
{
    manager.removeChangeListener(this);
}

void ChannelSelectorListBox::changeListenerCallback(juce::ChangeBroadcaster*)
{
    repaint();

    auto* currentDevice = manager.getCurrentAudioDevice();

    if (device == currentDevice)
        return;

    channels.clear();

    if (!currentDevice)
        return;

    channels = currentDevice->getInputChannelNames();
    device = currentDevice;

    if (useStereoPairs)
    {
        juce::StringArray pairs;

        int numPairs = channels.size() / 2;
        for (int i = 0; i < numPairs; i++)
            pairs.add(getNameForChannelPair(channels[i * 2], channels[i * 2 + 1]));

        // Interestingly, StringArray->end() caused an issue on Mac
        if (channels.size() % 2)
            pairs.add(channels[channels.size() - 1].trim());

        channels = pairs;
    }

    updateContent();
}

int ChannelSelectorListBox::getNumRows()
{
    return channels.size();
}

void ChannelSelectorListBox::listBoxItemClicked(int row, const juce::MouseEvent& /*e*/)
{
    if (row < channels.size())
        flipEnablement(row);
}

void ChannelSelectorListBox::paintListBoxItem(int row, juce::Graphics& g, int width, int height, bool)
{
    using namespace juce;

    if (row >= channels.size())
        return;

    auto item = channels[row];
    auto inputChannels = manager.getAudioDeviceSetup().inputChannels;

    // We need to actually check the device for enablement
    bool enabled = useStereoPairs ? inputChannels[row * 2] || inputChannels[row * 2 + 1] : inputChannels[row];

    Rectangle<float> bounds{ float(width), float(height) };
    bounds = bounds.reduced(bounds.getHeight() * 0.04f);

    // To draw the tick box, we create a temporary ToggleButton (due to how I implemented the lnf function)
    ToggleButton tempComp;
    tempComp.setBounds(bounds.removeFromLeft(bounds.getHeight()).toNearestInt());
    getLookAndFeel().drawTickBox(g, tempComp, 0, 0, 0, 0, enabled, true, true, false);

    bounds.removeFromLeft(bounds.getHeight() * 0.3f);

    g.setColour(findColour(textColourId));
    g.setFont(getInter().withHeight(bounds.getHeight() * 0.85f));
    g.drawText(channels[row], bounds, Justification::centredLeft);
}

void ChannelSelectorListBox::flipEnablement(int row) const
{
    auto config = manager.getAudioDeviceSetup();
    config.useDefaultInputChannels = false;

    if (useStereoPairs)
    {
        bool enabled = config.inputChannels[row * 2] || config.inputChannels[row * 2 + 1];
        setBit(config.inputChannels, row * 2 + 1, !enabled);
        setBit(config.inputChannels, row * 2, !enabled);
    }
    else
    {
        setBit(config.inputChannels, row, !config.inputChannels[row]);
    }

    manager.setAudioDeviceSetup(config, true);
}

void ChannelSelectorListBox::setBit(juce::BigInteger& ch, int index, bool set) const
{
    auto numActive = ch.countNumberOfSetBits();

    if (ch[index] == set || (!set && numActive <= minInputChannels))
        return;

    // Clear a bit if necessary
    if (set && numActive >= maxInputChannels)
    {
        auto firstActiveChan = ch.findNextSetBit(0);
        ch.clearBit(index > firstActiveChan ? firstActiveChan : ch.getHighestBit());
    }

    ch.setBit(index, set);
}

juce::String ChannelSelectorListBox::getNameForChannelPair(const juce::String& name1, const juce::String& name2) const
{
    juce::String commonBit;

    for (int j = 0; j < name1.length(); ++j)
        if (name1.substring(0, j).equalsIgnoreCase(name2.substring(0, j)))
            commonBit = name1.substring(0, j);

    // Make sure we only split the name at a space, because otherwise, things like "input 11" + "input 12" would become "input 11 + 2"
    while (commonBit.isNotEmpty() && !juce::CharacterFunctions::isWhitespace(commonBit.getLastCharacter()))
        commonBit = commonBit.dropLastCharacters(1);

    return name1.trim() + " + " + name2.substring(commonBit.length()).trim();
}

//==============================================================================
InputDeviceSelector::InputDeviceSelector(juce::AudioDeviceManager& deviceManager, int minInputChannelsToUse, int maxInputChannelsToUse, bool showChannelsAsStereoPairs) :
    manager(deviceManager),
    titleLabel("", "Input Device Settings"),
    warningLabel("", "No device opened, check DAW audio settings if issues persist"),
    deviceTypeLabel("", "Type: "),
    deviceLabel("", "Device: "),
    channelsLabel("", "Channels: "),
    sampleRateLabel("", "Sample rate: "),
    bufferSizeLabel("", "Buffer size: "),

    levelMeter(deviceManager),
    channelSelector(deviceManager, minInputChannelsToUse, maxInputChannelsToUse, showChannelsAsStereoPairs)
{
    jassert(minInputChannelsToUse >= 0 && minInputChannelsToUse <= maxInputChannelsToUse);

    titleLabel.setJustificationType(juce::Justification::centred);

    juce::Array labels = { &titleLabel, &deviceTypeLabel, &deviceLabel, &channelsLabel, &sampleRateLabel, &bufferSizeLabel };
    for (auto* label : labels)
        addAndMakeVisible(label);

    warningLabel.setJustificationType(juce::Justification::centred);
    addChildComponent(warningLabel);

    const auto& types= manager.getAvailableDeviceTypes();
    for (int i = 0; i < types.size(); ++i)
        deviceTypeChooser.addItem(types.getUnchecked(i)->getTypeName(), i + 1);
    deviceTypeChooser.setTextWhenNoChoicesAvailable("No devices available");
    deviceTypeChooser.setSelectedId(1);

    juce::Array comboBoxes = { &deviceTypeChooser, &deviceChooser, &sampleRateChooser, &bufferSizeChooser };
    for (auto* box : comboBoxes)
    {
        box->onChange = [this] { settingsChanged(); };
        addAndMakeVisible(box);
    }

    addAndMakeVisible(levelMeter);
    addAndMakeVisible(channelSelector);

    manager.addChangeListener(this);
    settingsChanged();
}

InputDeviceSelector::~InputDeviceSelector()
{
    manager.removeChangeListener(this);
}

void InputDeviceSelector::paint(juce::Graphics& g)
{
    auto colors = getTheme();

    g.fillAll(colors.prompt);
    g.setColour(colors.foreground);
    g.fillRoundedRectangle(getLocalBounds().toFloat(), 5.f);
}

void InputDeviceSelector::resized()
{
    auto bounds = getLocalBounds().toFloat();
    auto w = bounds.getWidth();

    const auto row = bounds.getHeight() * 0.07f;
    const auto padding = bounds.getHeight() * 0.035f;
    constexpr auto labelProp = 0.25f;

    bounds.reduce(w * 0.12f, w * 0.05f);

    auto titleBounds = bounds.removeFromTop(bounds.proportionOfHeight(0.1f));
    titleLabel.setFont(getInter().withHeight(titleBounds.getHeight()));
    titleLabel.setBounds(titleBounds.toNearestInt());
    bounds.removeFromTop(padding / 4);

    auto warningLabelHeight = std::round(row * 0.665f);
    warningLabel.setFont(getInter().withHeight(warningLabelHeight));
    warningLabel.setBounds(bounds.removeFromTop(warningLabelHeight).toNearestInt());
    bounds.removeFromTop(padding);

    auto deviceTypeBounds = bounds.removeFromTop(row);
    deviceTypeLabel.setFont(getInter().withHeight(std::round(row * 0.95f)));
    deviceTypeLabel.setBounds(deviceTypeBounds.removeFromLeft(deviceTypeBounds.proportionOfWidth(labelProp)).toNearestInt());
    deviceTypeChooser.setBounds(deviceTypeBounds.withHeight(row * 1.2f).toNearestInt());
    bounds.removeFromTop(padding);

    auto deviceBounds = bounds.removeFromTop(row);
    deviceLabel.setFont(getInter().withHeight(std::round(row * 0.95f)));
    deviceLabel.setBounds(deviceBounds.removeFromLeft(deviceBounds.proportionOfWidth(labelProp)).toNearestInt());
    deviceChooser.setBounds(deviceBounds.removeFromLeft(bounds.proportionOfWidth(0.56f)).withHeight(row * 1.2f).toNearestInt());
    levelMeter.setBounds(deviceBounds.removeFromRight(bounds.proportionOfWidth(0.15f)).toNearestInt());
    bounds.removeFromTop(padding);

    auto channelsBounds = bounds.removeFromTop(5 * row);
    channelsLabel.setFont(getInter().withHeight(std::round(row * 0.95f)));
    channelsLabel.setBounds(channelsBounds.removeFromLeft(channelsBounds.proportionOfWidth(labelProp)).removeFromTop(row).toNearestInt());
    channelSelector.setBounds(channelsBounds.toNearestInt());
    channelSelector.setRowHeight(int(std::round(row)));
    bounds.removeFromTop(padding);

    auto lastRow = row * 0.7f;
    auto sampleRateBounds = bounds.removeFromTop(lastRow);
    auto bufferSizeBounds = sampleRateBounds.removeFromRight(sampleRateBounds.proportionOfWidth(0.55f));

    auto sampleRateLabelBounds = sampleRateBounds.removeFromLeft(sampleRateBounds.proportionOfWidth(0.5f));
    sampleRateLabel.setFont(getInter().withHeight(std::round(lastRow * 0.95f)));
    sampleRateLabel.setBounds(sampleRateLabelBounds.toNearestInt());
    sampleRateChooser.setBounds(sampleRateBounds.withHeight(lastRow * 1.2f).toNearestInt());
    bufferSizeBounds.removeFromLeft(bounds.getWidth() * 0.05f);

    bufferSizeLabel.setFont(getInter().withHeight(std::round(lastRow * 0.95f)));
    bufferSizeLabel.setBounds(bufferSizeBounds.removeFromLeft(bufferSizeBounds.proportionOfWidth(0.5f)).toNearestInt());
    bufferSizeChooser.setBounds(bufferSizeBounds.withHeight(lastRow * 1.2f).toNearestInt());
}

void InputDeviceSelector::lookAndFeelChanged()
{
    auto colors = getTheme();

    warningLabel.setColour(juce::Label::textColourId, colors.highlight);

    juce::Array comboBoxes = { &deviceTypeChooser, &deviceChooser, &sampleRateChooser, &bufferSizeChooser };
    for (auto* box : comboBoxes)
        box->setColour(Colors::backgroundColorId, colors.foreground);
}

void InputDeviceSelector::changeListenerCallback(juce::ChangeBroadcaster*)
{
    auto* device = manager.getCurrentAudioDevice();

    warningLabel.setVisible(!device);
    if (!device)
        return;
 
    inputDevice = device;
    deviceChooser.setText(device->getName());

    sampleRateChooser.clear();
    for (auto rate : device->getAvailableSampleRates())
        sampleRateChooser.addItem(juce::String(rate) + " hz", int(rate));
    sampleRateChooser.setText(juce::String(int(device->getCurrentSampleRate())) + " hz");

    bufferSizeChooser.clear();
    for (auto size : device->getAvailableBufferSizes())
        bufferSizeChooser.addItem(juce::String(size), int(size));
    bufferSizeChooser.setText(juce::String(device->getCurrentBufferSizeSamples()));

    updateEnablement();
}

void InputDeviceSelector::settingsChanged()
{
    juce::AudioDeviceManager::AudioDeviceSetup settings = manager.getAudioDeviceSetup();

    auto deviceType = manager.getCurrentAudioDeviceType();
    auto* selectedDeviceType = manager.getAvailableDeviceTypes()[deviceTypeChooser.getSelectedId() - 1];
    bool newDeviceSelected = false;
    if (deviceType != selectedDeviceType->getTypeName() || !bool(deviceChooser.getNumItems()))
    {
        manager.setCurrentAudioDeviceType(selectedDeviceType->getTypeName(), true);

        deviceChooser.clear();
        auto devices = selectedDeviceType->getDeviceNames(true);
        for (int i = 0; i < devices.size(); i++)
            deviceChooser.addItem(devices[i], i + 1);
        deviceChooser.setSelectedId(1);
        newDeviceSelected = true;
    }

    auto device = settings.inputDeviceName;
    auto selectedDevice = deviceChooser.getItemText(deviceChooser.getSelectedItemIndex());
    if (device != selectedDevice || newDeviceSelected)
    {
        settings.inputDeviceName = selectedDevice;
        settings.useDefaultInputChannels = true;
    }

    auto sampleRate = int(settings.sampleRate);
    auto selectedSampleRate = sampleRateChooser.getSelectedId();
    if (sampleRate != selectedSampleRate && !newDeviceSelected && selectedSampleRate)
        settings.sampleRate = selectedSampleRate;

    auto bufferSize = settings.bufferSize;
    auto selectedBufferSize = bufferSizeChooser.getSelectedId();
    if (bufferSize != selectedBufferSize && !newDeviceSelected && selectedBufferSize)
        settings.bufferSize = selectedBufferSize;

    manager.setAudioDeviceSetup(settings, true);

    updateEnablement();
}

void InputDeviceSelector::updateEnablement()
{
    deviceChooser.setEnabled(bool(deviceChooser.getNumItems()));
    sampleRateChooser.setEnabled(bool(sampleRateChooser.getNumItems()) && inputDevice && manager.getAudioDeviceSetup().inputChannels.countNumberOfSetBits());
    bufferSizeChooser.setEnabled(bool(bufferSizeChooser.getNumItems()) && inputDevice && manager.getAudioDeviceSetup().inputChannels.countNumberOfSetBits());

    levelMeter.setEnabled(bool(inputDevice));
    channelSelector.setVisible(bool(inputDevice));
}
