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

   This file is part of the JUCE library.
   Copyright (c) 2015 - ROLI Ltd.

   Permission is granted to use this software under the terms of either:
   a) the GPL v2 (or any later version)
   b) the Affero GPL v3

   Details of these licenses can be found at: www.gnu.org/licenses

   JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
   A PARTICULAR PURPOSE.  See the GNU General Public License for more details.

   ------------------------------------------------------------------------------

   To release a closed-source product which uses JUCE, commercial licenses are
   available: visit www.juce.com for more information.

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

#if JUCE_ALSA

// You can define these strings in your app if you want to override the default names:
#ifndef JUCE_ALSA_MIDI_INPUT_NAME
 #define JUCE_ALSA_MIDI_INPUT_NAME  "Juce Midi Input"
#endif

#ifndef JUCE_ALSA_MIDI_OUTPUT_NAME
 #define JUCE_ALSA_MIDI_OUTPUT_NAME "Juce Midi Output"
#endif

//==============================================================================
namespace
{

class AlsaPortAndCallback;

//==============================================================================
class AlsaClient : public ReferenceCountedObject
{
public:
    typedef ReferenceCountedObjectPtr<AlsaClient> Ptr;

    AlsaClient (bool forInput)
        : input (forInput), handle (nullptr)
    {
        snd_seq_open (&handle, "default", forInput ? SND_SEQ_OPEN_INPUT
                                                   : SND_SEQ_OPEN_OUTPUT, 0);
    }

    ~AlsaClient()
    {
        if (handle != nullptr)
        {
            snd_seq_close (handle);
            handle = nullptr;
        }

        jassert (activeCallbacks.size() == 0);

        if (inputThread)
        {
            inputThread->stopThread (3000);
            inputThread = nullptr;
        }
    }

    bool isInput() const noexcept    { return input; }

    void setName (const String& name)
    {
        snd_seq_set_client_name (handle, name.toUTF8());
    }

    void registerCallback (AlsaPortAndCallback* cb)
    {
        if (cb != nullptr)
        {
            {
                const ScopedLock sl (callbackLock);
                activeCallbacks.add (cb);

                if (inputThread == nullptr)
                    inputThread = new MidiInputThread (*this);
            }

            inputThread->startThread();
        }
    }

    void unregisterCallback (AlsaPortAndCallback* cb)
    {
        const ScopedLock sl (callbackLock);

        jassert (activeCallbacks.contains (cb));
        activeCallbacks.removeAllInstancesOf (cb);

        if (activeCallbacks.size() == 0 && inputThread->isThreadRunning())
            inputThread->signalThreadShouldExit();
    }

    void handleIncomingMidiMessage (const MidiMessage& message, int port);

    snd_seq_t* get() const noexcept     { return handle; }

private:
    bool input;
    snd_seq_t* handle;

    Array<AlsaPortAndCallback*> activeCallbacks;
    CriticalSection callbackLock;

    //==============================================================================
    class MidiInputThread   : public Thread
    {
    public:
        MidiInputThread (AlsaClient& c)
            : Thread ("Juce MIDI Input"), client (c)
        {
            jassert (client.input && client.get() != nullptr);
        }

        void run() override
        {
            const int maxEventSize = 16 * 1024;
            snd_midi_event_t* midiParser;
            snd_seq_t* seqHandle = client.get();

            if (snd_midi_event_new (maxEventSize, &midiParser) >= 0)
            {
                const int numPfds = snd_seq_poll_descriptors_count (seqHandle, POLLIN);
                HeapBlock<pollfd> pfd ((size_t) numPfds);
                snd_seq_poll_descriptors (seqHandle, pfd, (unsigned int) numPfds, POLLIN);

                HeapBlock <uint8> buffer (maxEventSize);

                while (! threadShouldExit())
                {
                    if (poll (pfd, (nfds_t) numPfds, 100) > 0) // there was a "500" here which is a bit long when we exit the program and have to wait for a timeout on this poll call
                    {
                        if (threadShouldExit())
                            break;

                        snd_seq_nonblock (seqHandle, 1);

                        do
                        {
                            snd_seq_event_t* inputEvent = nullptr;

                            if (snd_seq_event_input (seqHandle, &inputEvent) >= 0)
                            {
                                // xxx what about SYSEXes that are too big for the buffer?
                                const long numBytes = snd_midi_event_decode (midiParser, buffer,
                                                                            maxEventSize, inputEvent);

                                snd_midi_event_reset_decode (midiParser);

                                if (numBytes > 0)
                                {
                                    const MidiMessage message ((const uint8*) buffer, (int) numBytes,
                                                               Time::getMillisecondCounter() * 0.001);

                                    client.handleIncomingMidiMessage (message, inputEvent->dest.port);
                                }

                                snd_seq_free_event (inputEvent);
                            }
                        }
                        while (snd_seq_event_input_pending (seqHandle, 0) > 0);
                    }
                }

                snd_midi_event_free (midiParser);
            }
        };

    private:
        AlsaClient& client;
    };

    ScopedPointer<MidiInputThread> inputThread;
};


static AlsaClient::Ptr globalAlsaSequencerIn()
{
    static AlsaClient::Ptr global (new AlsaClient (true));
    return global;
}

static AlsaClient::Ptr globalAlsaSequencerOut()
{
    static AlsaClient::Ptr global (new AlsaClient (false));
    return global;
}

static AlsaClient::Ptr globalAlsaSequencer (bool input)
{
    return input ? globalAlsaSequencerIn()
                 : globalAlsaSequencerOut();
}

//==============================================================================
// represents an input or output port of the supplied AlsaClient
class AlsaPort
{
public:
    AlsaPort() noexcept  : portId (-1)  {}
    AlsaPort (const AlsaClient::Ptr& c, int port) noexcept  : client (c), portId (port) {}

    void createPort (const AlsaClient::Ptr& c, const String& name, bool forInput)
    {
        client = c;

        if (snd_seq_t* handle = client->get())
            portId = snd_seq_create_simple_port (handle, name.toUTF8(),
                                                 forInput ? (SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE)
                                                          : (SND_SEQ_PORT_CAP_READ  | SND_SEQ_PORT_CAP_SUBS_READ),
                                                 SND_SEQ_PORT_TYPE_MIDI_GENERIC);
    }

    void deletePort()
    {
        if (isValid())
        {
            snd_seq_delete_simple_port (client->get(), portId);
            portId = -1;
        }
    }

    void connectWith (int sourceClient, int sourcePort)
    {
        if (client->isInput())
            snd_seq_connect_from (client->get(), portId, sourceClient, sourcePort);
        else
            snd_seq_connect_to (client->get(), portId, sourceClient, sourcePort);
    }

    bool isValid() const noexcept
    {
        return client != nullptr && client->get() != nullptr && portId >= 0;
    }

    AlsaClient::Ptr client;
    int portId;
};

//==============================================================================
class AlsaPortAndCallback
{
public:
    AlsaPortAndCallback (AlsaPort p, MidiInput* in, MidiInputCallback* cb)
        : port (p), midiInput (in), callback (cb), callbackEnabled (false)
    {
    }

    ~AlsaPortAndCallback()
    {
        enableCallback (false);
        port.deletePort();
    }

    void enableCallback (bool enable)
    {
        if (callbackEnabled != enable)
        {
            callbackEnabled = enable;

            if (enable)
                port.client->registerCallback (this);
            else
                port.client->unregisterCallback (this);
        }
    }

    void handleIncomingMidiMessage (const MidiMessage& message) const
    {
        callback->handleIncomingMidiMessage (midiInput, message);
    }

private:
    AlsaPort port;
    MidiInput* midiInput;
    MidiInputCallback* callback;
    bool callbackEnabled;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AlsaPortAndCallback)
};

void AlsaClient::handleIncomingMidiMessage (const MidiMessage& message, int port)
{
    const ScopedLock sl (callbackLock);

    if (AlsaPortAndCallback* const cb = activeCallbacks[port])
        cb->handleIncomingMidiMessage (message);
}

//==============================================================================
static AlsaPort iterateMidiClient (const AlsaClient::Ptr& seq,
                                   snd_seq_client_info_t* clientInfo,
                                   const bool forInput,
                                   StringArray& deviceNamesFound,
                                   const int deviceIndexToOpen)
{
    AlsaPort port;

    snd_seq_t* seqHandle = seq->get();
    snd_seq_port_info_t* portInfo = nullptr;

    if (snd_seq_port_info_malloc (&portInfo) == 0)
    {
        int numPorts = snd_seq_client_info_get_num_ports (clientInfo);
        const int client = snd_seq_client_info_get_client (clientInfo);

        snd_seq_port_info_set_client (portInfo, client);
        snd_seq_port_info_set_port (portInfo, -1);

        while (--numPorts >= 0)
        {
            if (snd_seq_query_next_port (seqHandle, portInfo) == 0
                && (snd_seq_port_info_get_capability (portInfo) & (forInput ? SND_SEQ_PORT_CAP_READ
                                                                            : SND_SEQ_PORT_CAP_WRITE)) != 0)
            {
                deviceNamesFound.add (snd_seq_client_info_get_name (clientInfo));

                if (deviceNamesFound.size() == deviceIndexToOpen + 1)
                {
                    const int sourcePort   = snd_seq_port_info_get_port (portInfo);
                    const int sourceClient = snd_seq_client_info_get_client (clientInfo);

                    if (sourcePort != -1)
                    {
                        const String name (forInput ? JUCE_ALSA_MIDI_INPUT_NAME
                                                    : JUCE_ALSA_MIDI_OUTPUT_NAME);
                        seq->setName (name);
                        port.createPort (seq, name, forInput);
                        port.connectWith (sourceClient, sourcePort);
                    }
                }
            }
        }

        snd_seq_port_info_free (portInfo);
    }

    return port;
}

static AlsaPort iterateMidiDevices (const bool forInput,
                                    StringArray& deviceNamesFound,
                                    const int deviceIndexToOpen)
{
    AlsaPort port;
    const AlsaClient::Ptr client (globalAlsaSequencer (forInput));

    if (snd_seq_t* const seqHandle = client->get())
    {
        snd_seq_system_info_t* systemInfo = nullptr;
        snd_seq_client_info_t* clientInfo = nullptr;

        if (snd_seq_system_info_malloc (&systemInfo) == 0)
        {
            if (snd_seq_system_info (seqHandle, systemInfo) == 0
                 && snd_seq_client_info_malloc (&clientInfo) == 0)
            {
                int numClients = snd_seq_system_info_get_cur_clients (systemInfo);

                while (--numClients >= 0 && ! port.isValid())
                    if (snd_seq_query_next_client (seqHandle, clientInfo) == 0)
                        port = iterateMidiClient (client, clientInfo, forInput,
                                                  deviceNamesFound, deviceIndexToOpen);

                snd_seq_client_info_free (clientInfo);
            }

            snd_seq_system_info_free (systemInfo);
        }

    }

    deviceNamesFound.appendNumbersToDuplicates (true, true);

    return port;
}

AlsaPort createMidiDevice (const bool forInput, const String& deviceNameToOpen)
{
    AlsaPort port;
    AlsaClient::Ptr client (new AlsaClient (forInput));

    if (client->get())
    {
        client->setName (deviceNameToOpen + (forInput ? " Input" : " Output"));
        port.createPort (client, forInput ? "in" : "out", forInput);
    }

    return port;
}

//==============================================================================
class MidiOutputDevice
{
public:
    MidiOutputDevice (MidiOutput* const output, const AlsaPort& p)
        : midiOutput (output), port (p),
          maxEventSize (16 * 1024)
    {
        jassert (port.isValid() && midiOutput != nullptr);
        snd_midi_event_new ((size_t) maxEventSize, &midiParser);
    }

    ~MidiOutputDevice()
    {
        snd_midi_event_free (midiParser);
        port.deletePort();
    }

    bool sendMessageNow (const MidiMessage& message)
    {
        if (message.getRawDataSize() > maxEventSize)
        {
            maxEventSize = message.getRawDataSize();
            snd_midi_event_free (midiParser);
            snd_midi_event_new ((size_t) maxEventSize, &midiParser);
        }

        snd_seq_event_t event;
        snd_seq_ev_clear (&event);

        long numBytes = (long) message.getRawDataSize();
        const uint8* data = message.getRawData();

        snd_seq_t* seqHandle = port.client->get();
        bool success = true;

        while (numBytes > 0)
        {
            const long numSent = snd_midi_event_encode (midiParser, data, numBytes, &event);

            if (numSent <= 0)
            {
                success = numSent == 0;
                break;
            }

            numBytes -= numSent;
            data += numSent;

            snd_seq_ev_set_source (&event, 0);
            snd_seq_ev_set_subs (&event);
            snd_seq_ev_set_direct (&event);

            if (snd_seq_event_output_direct (seqHandle, &event) < 0)
            {
                success = false;
                break;
            }
        }

        snd_midi_event_reset_encode (midiParser);
        return success;
    }

private:
    MidiOutput* const midiOutput;
    AlsaPort port;
    snd_midi_event_t* midiParser;
    int maxEventSize;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiOutputDevice);
};

} // namespace

StringArray MidiOutput::getDevices()
{
    StringArray devices;
    iterateMidiDevices (false, devices, -1);
    return devices;
}

int MidiOutput::getDefaultDeviceIndex()
{
    return 0;
}

MidiOutput* MidiOutput::openDevice (int deviceIndex)
{
    MidiOutput* newDevice = nullptr;

    StringArray devices;
    AlsaPort port (iterateMidiDevices (false, devices, deviceIndex));

    if (port.isValid())
    {
        newDevice = new MidiOutput();
        newDevice->internal = new MidiOutputDevice (newDevice, port);
    }

    return newDevice;
}

MidiOutput* MidiOutput::createNewDevice (const String& deviceName)
{
    MidiOutput* newDevice = nullptr;

    AlsaPort port (createMidiDevice (false, deviceName));

    if (port.isValid())
    {
        newDevice = new MidiOutput();
        newDevice->internal = new MidiOutputDevice (newDevice, port);
    }

    return newDevice;
}

MidiOutput::~MidiOutput()
{
    stopBackgroundThread();

    delete static_cast<MidiOutputDevice*> (internal);
}

void MidiOutput::sendMessageNow (const MidiMessage& message)
{
    static_cast<MidiOutputDevice*> (internal)->sendMessageNow (message);
}

//==============================================================================
MidiInput::MidiInput (const String& nm)
    : name (nm), internal (nullptr)
{
}

MidiInput::~MidiInput()
{
    stop();
    delete static_cast<AlsaPortAndCallback*> (internal);
}

void MidiInput::start()
{
    static_cast<AlsaPortAndCallback*> (internal)->enableCallback (true);
}

void MidiInput::stop()
{
    static_cast<AlsaPortAndCallback*> (internal)->enableCallback (false);
}

int MidiInput::getDefaultDeviceIndex()
{
    return 0;
}

StringArray MidiInput::getDevices()
{
    StringArray devices;
    iterateMidiDevices (true, devices, -1);
    return devices;
}

MidiInput* MidiInput::openDevice (int deviceIndex, MidiInputCallback* callback)
{
    MidiInput* newDevice = nullptr;

    StringArray devices;
    AlsaPort port (iterateMidiDevices (true, devices, deviceIndex));

    if (port.isValid())
    {
        newDevice = new MidiInput (devices [deviceIndex]);
        newDevice->internal = new AlsaPortAndCallback (port, newDevice, callback);
    }

    return newDevice;
}

MidiInput* MidiInput::createNewDevice (const String& deviceName, MidiInputCallback* callback)
{
    MidiInput* newDevice = nullptr;

    AlsaPort port (createMidiDevice (true, deviceName));

    if (port.isValid())
    {
        newDevice = new MidiInput (deviceName);
        newDevice->internal = new AlsaPortAndCallback (port, newDevice, callback);
    }

    return newDevice;
}


//==============================================================================
#else

// (These are just stub functions if ALSA is unavailable...)

StringArray MidiOutput::getDevices()                                { return StringArray(); }
int MidiOutput::getDefaultDeviceIndex()                             { return 0; }
MidiOutput* MidiOutput::openDevice (int)                            { return nullptr; }
MidiOutput* MidiOutput::createNewDevice (const String&)             { return nullptr; }
MidiOutput::~MidiOutput()   {}
void MidiOutput::sendMessageNow (const MidiMessage&)    {}

MidiInput::MidiInput (const String& nm) : name (nm), internal (nullptr)  {}
MidiInput::~MidiInput() {}
void MidiInput::start() {}
void MidiInput::stop()  {}
int MidiInput::getDefaultDeviceIndex()      { return 0; }
StringArray MidiInput::getDevices()         { return StringArray(); }
MidiInput* MidiInput::openDevice (int, MidiInputCallback*)                  { return nullptr; }
MidiInput* MidiInput::createNewDevice (const String&, MidiInputCallback*)   { return nullptr; }

#endif
