/*
Copyright (C) 2011 Devin Anderson

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program 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.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

*/

#include <memory>
#include <new>
#include <stdexcept>

#include <alsa/asoundlib.h>

#include "JackALSARawMidiDriver.h"
#include "JackALSARawMidiUtil.h"
#include "JackEngineControl.h"
#include "JackError.h"
#include "JackMidiUtil.h"
#include "driver_interface.h"

using Jack::JackALSARawMidiDriver;

JackALSARawMidiDriver::JackALSARawMidiDriver(const char *name,
                                             const char *alias,
                                             JackLockedEngine *engine,
                                             JackSynchro *table):
    JackMidiDriver(name, alias, engine, table)
{
    thread = new JackThread(this);
    fds[0] = -1;
    fds[1] = -1;
    input_ports = 0;
    output_ports = 0;
    output_port_timeouts = 0;
    poll_fds = 0;
}

JackALSARawMidiDriver::~JackALSARawMidiDriver()
{
    delete thread;
}

int
JackALSARawMidiDriver::Attach()
{
    const char *alias;
    jack_nframes_t buffer_size = fEngineControl->fBufferSize;
    jack_port_id_t index;
    jack_nframes_t latency = buffer_size;
    jack_latency_range_t latency_range;
    const char *name;
    JackPort *port;
    latency_range.max = latency;
    latency_range.min = latency;
    for (int i = 0; i < fCaptureChannels; i++) {
        JackALSARawMidiInputPort *input_port = input_ports[i];
        name = input_port->GetName();
        fEngine->PortRegister(fClientControl.fRefNum, name,
                            JACK_DEFAULT_MIDI_TYPE,
                            CaptureDriverFlags, buffer_size, &index);
        if (index == NO_PORT) {
            jack_error("JackALSARawMidiDriver::Attach - cannot register input "
                       "port with name '%s'.", name);
            // XX: Do we need to deallocate ports?
            return -1;
        }
        alias = input_port->GetAlias();
        port = fGraphManager->GetPort(index);
        port->SetAlias(alias);
        port->SetLatencyRange(JackCaptureLatency, &latency_range);
        fEngine->PortSetDefaultMetadata(fClientControl.fRefNum, index,
                                        input_port->GetDeviceName());
        fCapturePortList[i] = index;

        jack_info("JackALSARawMidiDriver::Attach - input port registered "
                  "(name='%s', alias='%s').", name, alias);
    }
    if (! fEngineControl->fSyncMode) {
        latency += buffer_size;
        latency_range.max = latency;
        latency_range.min = latency;
    }
    for (int i = 0; i < fPlaybackChannels; i++) {
        JackALSARawMidiOutputPort *output_port = output_ports[i];
        name = output_port->GetName();
        fEngine->PortRegister(fClientControl.fRefNum, name,
                            JACK_DEFAULT_MIDI_TYPE,
                            PlaybackDriverFlags, buffer_size, &index);
        if (index == NO_PORT) {
            jack_error("JackALSARawMidiDriver::Attach - cannot register "
                       "output port with name '%s'.", name);
            // XX: Do we need to deallocate ports?
            return -1;
        }
        alias = output_port->GetAlias();
        port = fGraphManager->GetPort(index);
        port->SetAlias(alias);
        port->SetLatencyRange(JackPlaybackLatency, &latency_range);
        fEngine->PortSetDefaultMetadata(fClientControl.fRefNum, index,
                                        output_port->GetDeviceName());
        fPlaybackPortList[i] = index;

        jack_info("JackALSARawMidiDriver::Attach - output port registered "
                  "(name='%s', alias='%s').", name, alias);
    }
    return 0;
}

int
JackALSARawMidiDriver::Close()
{
    // Generic MIDI driver close
    int result = JackMidiDriver::Close();

    if (input_ports) {
        for (int i = 0; i < fCaptureChannels; i++) {
            delete input_ports[i];
        }
        delete[] input_ports;
        input_ports = 0;
    }
    if (output_ports) {
        for (int i = 0; i < fPlaybackChannels; i++) {
            delete output_ports[i];
        }
        delete[] output_ports;
        output_ports = 0;
    }
    return result;
}

bool
JackALSARawMidiDriver::Execute()
{
    jack_nframes_t timeout_frame = 0;
    for (;;) {
        struct timespec timeout;
        struct timespec *timeout_ptr;
        if (! timeout_frame) {
            timeout_ptr = 0;
        } else {

            // The timeout value is relative to the time that
            // 'GetMicroSeconds()' is called, not the time that 'poll()' is
            // called.  This means that the amount of time that passes between
            // 'GetMicroSeconds()' and 'ppoll()' is time that will be lost
            // while waiting for 'poll() to timeout.
            //
            // I tried to replace the timeout with a 'timerfd' with absolute
            // times, but, strangely, it actually slowed things down, and made
            // the code a lot more complicated.
            //
            // I wonder about using the 'epoll' interface instead of 'ppoll()'.
            // The problem with the 'epoll' interface is that the timeout
            // resolution of 'epoll_wait()' is set in milliseconds.  We need
            // microsecond resolution.  Without microsecond resolution, we
            // impose the same jitter as USB MIDI.
            //
            // Another problem is that 'ppoll()' returns later than the wait
            // time.  The problem can be minimized with high precision timers.

            timeout_ptr = &timeout;
            jack_time_t next_time = GetTimeFromFrames(timeout_frame);
            jack_time_t now = GetMicroSeconds();
            if (next_time <= now) {
                timeout.tv_sec = 0;
                timeout.tv_nsec = 0;
            } else {
                jack_time_t wait_time = next_time - now;
                timeout.tv_sec = wait_time / 1000000;
                timeout.tv_nsec = (wait_time % 1000000) * 1000;
            }
        }
        int poll_result = ppoll(poll_fds, poll_fd_count, timeout_ptr, 0);

        // Getting the current frame value here allows us to use it for
        // incoming MIDI bytes.  This makes sense, as the data has already
        // arrived at this point.
        jack_nframes_t current_frame = GetCurrentFrame();

        if (poll_result == -1) {
            if (errno == EINTR) {
                continue;
            }
            jack_error("JackALSARawMidiDriver::Execute - poll error: %s",
                       strerror(errno));
            break;
        }
        jack_nframes_t port_timeout;
        timeout_frame = 0;
        if (! poll_result) {

            // No I/O events occurred.  So, only handle timeout events on
            // output ports.

            for (int i = 0; i < fPlaybackChannels; i++) {
                port_timeout = output_port_timeouts[i];
                if (port_timeout && (port_timeout <= current_frame)) {
                    if (! output_ports[i]->ProcessPollEvents(false, true,
                                                             &port_timeout)) {
                        jack_error("JackALSARawMidiDriver::Execute - a fatal "
                                   "error occurred while processing ALSA "
                                   "output events.");
                        goto cleanup;
                    }
                    output_port_timeouts[i] = port_timeout;
                }
                if (port_timeout && ((! timeout_frame) ||
                                     (port_timeout < timeout_frame))) {
                    timeout_frame = port_timeout;
                }
            }
            continue;
        }

        // See if it's time to shutdown.

        unsigned short revents = poll_fds[0].revents;
        if (revents) {
            if (revents & (~ POLLHUP)) {
                jack_error("JackALSARawMidiDriver::Execute - unexpected poll "
                           "event on pipe file descriptor.");
            }
            break;
        }

        // Handle I/O events *and* timeout events on output ports.

        for (int i = 0; i < fPlaybackChannels; i++) {
            port_timeout = output_port_timeouts[i];
            bool timeout = port_timeout && (port_timeout <= current_frame);
            if (! output_ports[i]->ProcessPollEvents(true, timeout,
                                                     &port_timeout)) {
                jack_error("JackALSARawMidiDriver::Execute - a fatal error "
                           "occurred while processing ALSA output events.");
                goto cleanup;
            }
            output_port_timeouts[i] = port_timeout;
            if (port_timeout && ((! timeout_frame) ||
                                 (port_timeout < timeout_frame))) {
                timeout_frame = port_timeout;
            }
        }

        // Handle I/O events on input ports.  We handle these last because we
        // already computed the arrival time above, and will impose a delay on
        // the events by 'period-size' frames anyway, which gives us a bit of
        // borrowed time.

        for (int i = 0; i < fCaptureChannels; i++) {
            if (! input_ports[i]->ProcessPollEvents(current_frame)) {
                jack_error("JackALSARawMidiDriver::Execute - a fatal error "
                           "occurred while processing ALSA input events.");
                goto cleanup;
            }
        }
    }
 cleanup:
    close(fds[0]);
    fds[0] = -1;

    jack_info("JackALSARawMidiDriver::Execute - ALSA thread exiting.");

    return false;
}

void
JackALSARawMidiDriver::
FreeDeviceInfo(std::vector<snd_rawmidi_info_t *> *in_info_list,
               std::vector<snd_rawmidi_info_t *> *out_info_list)
{
    size_t length = in_info_list->size();
    for (size_t i = 0; i < length; i++) {
        snd_rawmidi_info_free(in_info_list->at(i));
    }
    length = out_info_list->size();
    for (size_t i = 0; i < length; i++) {
        snd_rawmidi_info_free(out_info_list->at(i));
    }
}

void
JackALSARawMidiDriver::
GetDeviceInfo(snd_ctl_t *control, snd_rawmidi_info_t *info,
              std::vector<snd_rawmidi_info_t *> *info_list)
{
    snd_rawmidi_info_set_subdevice(info, 0);
    int code = snd_ctl_rawmidi_info(control, info);
    if (code) {
        if (code != -ENOENT) {
            HandleALSAError("GetDeviceInfo", "snd_ctl_rawmidi_info", code);
        }
        return;
    }
    unsigned int count = snd_rawmidi_info_get_subdevices_count(info);
    for (unsigned int i = 0; i < count; i++) {
        snd_rawmidi_info_set_subdevice(info, i);
        int code = snd_ctl_rawmidi_info(control, info);
        if (code) {
            HandleALSAError("GetDeviceInfo", "snd_ctl_rawmidi_info", code);
            continue;
        }
        snd_rawmidi_info_t *info_copy;
        code = snd_rawmidi_info_malloc(&info_copy);
        if (code) {
            HandleALSAError("GetDeviceInfo", "snd_rawmidi_info_malloc", code);
            continue;
        }
        snd_rawmidi_info_copy(info_copy, info);
        try {
            info_list->push_back(info_copy);
        } catch (std::bad_alloc &e) {
            snd_rawmidi_info_free(info_copy);
            jack_error("JackALSARawMidiDriver::GetDeviceInfo - "
                       "std::vector::push_back: %s", e.what());
        }
    }
}

void
JackALSARawMidiDriver::HandleALSAError(const char *driver_func,
                                       const char *alsa_func, int code)
{
    jack_error("JackALSARawMidiDriver::%s - %s: %s", driver_func, alsa_func,
               snd_strerror(code));
}

bool
JackALSARawMidiDriver::Init()
{
    set_threaded_log_function();
    if (thread->AcquireSelfRealTime(fEngineControl->fServerPriority + 1)) {
        jack_error("JackALSARawMidiDriver::Init - could not acquire realtime "
                   "scheduling.  Continuing anyway.");
    }
    return true;
}

int
JackALSARawMidiDriver::Open(bool capturing, bool playing, int in_channels,
                            int out_channels, bool monitor,
                            const char *capture_driver_name,
                            const char *playback_driver_name,
                            jack_nframes_t capture_latency,
                            jack_nframes_t playback_latency)
{
    snd_rawmidi_info_t *info;
    int code = snd_rawmidi_info_malloc(&info);
    if (code) {
        HandleALSAError("Open", "snd_rawmidi_info_malloc", code);
        return -1;
    }
    std::vector<snd_rawmidi_info_t *> in_info_list;
    std::vector<snd_rawmidi_info_t *> out_info_list;
    for (int card = -1;;) {
        int code = snd_card_next(&card);
        if (code) {
            HandleALSAError("Open", "snd_card_next", code);
            continue;
        }
        if (card == -1) {
            break;
        }
        char name[32];
        snprintf(name, sizeof(name), "hw:%d", card);
        snd_ctl_t *control;
        code = snd_ctl_open(&control, name, SND_CTL_NONBLOCK);
        if (code) {
            HandleALSAError("Open", "snd_ctl_open", code);
            continue;
        }
        for (int device = -1;;) {
            code = snd_ctl_rawmidi_next_device(control, &device);
            if (code) {
                HandleALSAError("Open", "snd_ctl_rawmidi_next_device", code);
                continue;
            }
            if (device == -1) {
                break;
            }
            snd_rawmidi_info_set_device(info, device);
            snd_rawmidi_info_set_stream(info, SND_RAWMIDI_STREAM_INPUT);
            GetDeviceInfo(control, info, &in_info_list);
            snd_rawmidi_info_set_stream(info, SND_RAWMIDI_STREAM_OUTPUT);
            GetDeviceInfo(control, info, &out_info_list);
        }
        snd_ctl_close(control);
    }
    snd_rawmidi_info_free(info);
    size_t potential_inputs = in_info_list.size();
    size_t potential_outputs = out_info_list.size();
    if (! (potential_inputs || potential_outputs)) {
        jack_error("JackALSARawMidiDriver::Open - no ALSA raw MIDI input or "
                   "output ports found.");
        FreeDeviceInfo(&in_info_list, &out_info_list);
        return -1;
    }
    size_t num_inputs = 0;
    size_t num_outputs = 0;
    const char *client_name = fClientControl.fName;
    if (potential_inputs) {
        try {
            input_ports = new JackALSARawMidiInputPort *[potential_inputs];
        } catch (std::exception& e) {
            jack_error("JackALSARawMidiDriver::Open - while creating input "
                       "port array: %s", e.what());
            FreeDeviceInfo(&in_info_list, &out_info_list);
            return -1;
        }
    }
    if (potential_outputs) {
        try {
            output_ports = new JackALSARawMidiOutputPort *[potential_outputs];
        } catch (std::exception& e) {
            jack_error("JackALSARawMidiDriver::Open - while creating output "
                       "port array: %s", e.what());
            FreeDeviceInfo(&in_info_list, &out_info_list);
            goto delete_input_ports;
        }
    }
    for (size_t i = 0; i < potential_inputs; i++) {
        snd_rawmidi_info_t *info = in_info_list.at(i);
        try {
            input_ports[num_inputs] = new JackALSARawMidiInputPort(client_name, info, i);
            num_inputs++;
        } catch (std::exception& e) {
            jack_error("JackALSARawMidiDriver::Open - while creating new "
                       "JackALSARawMidiInputPort: %s", e.what());
        }
        snd_rawmidi_info_free(info);
    }
    for (size_t i = 0; i < potential_outputs; i++) {
        snd_rawmidi_info_t *info = out_info_list.at(i);
        try {
            output_ports[num_outputs] = new JackALSARawMidiOutputPort(client_name, info, i);
            num_outputs++;
        } catch (std::exception& e) {
            jack_error("JackALSARawMidiDriver::Open - while creating new "
                       "JackALSARawMidiOutputPort: %s", e.what());
        }
        snd_rawmidi_info_free(info);
    }
    if (! (num_inputs || num_outputs)) {
        jack_error("JackALSARawMidiDriver::Open - none of the potential "
                   "inputs or outputs were successfully opened.");
    } else if (JackMidiDriver::Open(capturing, playing, num_inputs,
                                    num_outputs, monitor, capture_driver_name,
                                    playback_driver_name, capture_latency,
                                    playback_latency)) {
        jack_error("JackALSARawMidiDriver::Open - JackMidiDriver::Open error");
    } else {
        return 0;
    }
    if (output_ports) {
        for (size_t i = 0; i < num_outputs; i++) {
            delete output_ports[i];
        }
        delete[] output_ports;
        output_ports = 0;
    }
 delete_input_ports:
    if (input_ports) {
        for (size_t i = 0; i < num_inputs; i++) {
            delete input_ports[i];
        }
        delete[] input_ports;
        input_ports = 0;
    }
    return -1;
}

int
JackALSARawMidiDriver::Read()
{
    jack_nframes_t buffer_size = fEngineControl->fBufferSize;
    for (int i = 0; i < fCaptureChannels; i++) {
        if (! input_ports[i]->ProcessJack(GetInputBuffer(i), buffer_size)) {
            return -1;
        }
    }
    return 0;
}

int
JackALSARawMidiDriver::Start()
{

    jack_info("JackALSARawMidiDriver::Start - Starting 'alsarawmidi' driver.");

    JackMidiDriver::Start();
    poll_fd_count = 1;
    for (int i = 0; i < fCaptureChannels; i++) {
        poll_fd_count += input_ports[i]->GetPollDescriptorCount();
    }
    for (int i = 0; i < fPlaybackChannels; i++) {
        poll_fd_count += output_ports[i]->GetPollDescriptorCount();
    }
    try {
        poll_fds = new pollfd[poll_fd_count];
    } catch (std::exception& e) {
        jack_error("JackALSARawMidiDriver::Start - creating poll descriptor "
                   "structures failed: %s", e.what());
        return -1;
    }
    if (fPlaybackChannels) {
        try {
            output_port_timeouts = new jack_nframes_t[fPlaybackChannels];
        } catch (std::exception& e) {
            jack_error("JackALSARawMidiDriver::Start - creating array for "
                       "output port timeout values failed: %s", e.what());
            goto free_poll_descriptors;
        }
    }
    struct pollfd *poll_fd_iter;
    try {
        CreateNonBlockingPipe(fds);
    } catch (std::exception& e) {
        jack_error("JackALSARawMidiDriver::Start - while creating wake pipe: "
                   "%s", e.what());
        goto free_output_port_timeouts;
    }
    poll_fds[0].events = POLLERR | POLLIN | POLLNVAL;
    poll_fds[0].fd = fds[0];
    poll_fd_iter = poll_fds + 1;
    for (int i = 0; i < fCaptureChannels; i++) {
        JackALSARawMidiInputPort *input_port = input_ports[i];
        input_port->PopulatePollDescriptors(poll_fd_iter);
        poll_fd_iter += input_port->GetPollDescriptorCount();
    }
    for (int i = 0; i < fPlaybackChannels; i++) {
        JackALSARawMidiOutputPort *output_port = output_ports[i];
        output_port->PopulatePollDescriptors(poll_fd_iter);
        poll_fd_iter += output_port->GetPollDescriptorCount();
        output_port_timeouts[i] = 0;
    }

    jack_info("JackALSARawMidiDriver::Start - starting ALSA thread ...");

    if (! thread->StartSync()) {

        jack_info("JackALSARawMidiDriver::Start - started ALSA thread.");

        return 0;
    }
    jack_error("JackALSARawMidiDriver::Start - failed to start MIDI "
               "processing thread.");

    DestroyNonBlockingPipe(fds);
    fds[1] = -1;
    fds[0] = -1;
 free_output_port_timeouts:
    delete[] output_port_timeouts;
    output_port_timeouts = 0;
 free_poll_descriptors:
    delete[] poll_fds;
    poll_fds = 0;
    return -1;
}

int
JackALSARawMidiDriver::Stop()
{
    jack_info("JackALSARawMidiDriver::Stop - stopping 'alsarawmidi' driver.");
    JackMidiDriver::Stop();

    if (fds[1] != -1) {
        close(fds[1]);
        fds[1] = -1;
    }
    int result;
    const char *verb;
    switch (thread->GetStatus()) {
    case JackThread::kIniting:
    case JackThread::kStarting:
        result = thread->Kill();
        verb = "kill";
        break;
    case JackThread::kRunning:
        result = thread->Stop();
        verb = "stop";
        break;
    default:
        result = 0;
        verb = 0;
    }
    if (fds[0] != -1) {
        close(fds[0]);
        fds[0] = -1;
    }
    if (output_port_timeouts) {
        delete[] output_port_timeouts;
        output_port_timeouts = 0;
    }
    if (poll_fds) {
        delete[] poll_fds;
        poll_fds = 0;
    }
    if (result) {
        jack_error("JackALSARawMidiDriver::Stop - could not %s MIDI "
                   "processing thread.", verb);
    }
    return result;
}

int
JackALSARawMidiDriver::Write()
{
    jack_nframes_t buffer_size = fEngineControl->fBufferSize;
    for (int i = 0; i < fPlaybackChannels; i++) {
        if (! output_ports[i]->ProcessJack(GetOutputBuffer(i), buffer_size)) {
            return -1;
        }
    }
    return 0;
}

#ifdef __cplusplus
extern "C" {
#endif
    
    // singleton kind of driver
    static Jack::JackALSARawMidiDriver* driver = NULL;

    SERVER_EXPORT jack_driver_desc_t *
    driver_get_descriptor()
    {
        // X: There could be parameters here regarding setting I/O buffer
        // sizes.  I don't think MIDI drivers can accept parameters right
        // now without being set as the main driver.

        return jack_driver_descriptor_construct("alsarawmidi", JackDriverSlave, "Alternative ALSA raw MIDI backend.", NULL);
    }

    SERVER_EXPORT Jack::JackDriverClientInterface *
    driver_initialize(Jack::JackLockedEngine *engine, Jack::JackSynchro *table,
                      const JSList *params)
    {
        // singleton kind of driver
        if (!driver) {
            driver = new Jack::JackALSARawMidiDriver("system_midi", "alsarawmidi", engine, table);
            if (driver->Open(1, 1, 0, 0, false, "midi in", "midi out", 0, 0) == 0) {
                return driver;
            } else {
                delete driver;
                return NULL;
            }
        } else {
            jack_info("JackALSARawMidiDriver already allocated, cannot be loaded twice");
            return NULL;
        }
    }

#ifdef __cplusplus
}
#endif
