/*
    JackEngine.cpp

    Copyright 2009, Alan Calvert

    This file is part of yoshimi, which 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 3 of the License, or (at your option) any later version.

    yoshimi 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 yoshimi.  If not, see <http://www.gnu.org/licenses/>.
*/

#include <iostream>

#include <jack/midiport.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <cassert>
#include <cstring>

#include "Nio.h"
#include "InMgr.h"

#include "JackEngine.h"

using namespace std;

extern char *instance_name;

JackEngine::JackEngine()
    :AudioOut(), jackClient(NULL)
{
    name = "JACK";
    audio.jackSamplerate = 0;
    audio.jackNframes    = 0;
    for(int i = 0; i < 2; ++i) {
        audio.ports[i]     = NULL;
        audio.portBuffs[i] = NULL;
    }
    midi.inport = NULL;
}

bool JackEngine::connectServer(string server)
{
    bool autostart_jack = true;
    if(jackClient)
        return true;

    string clientname = "zynaddsubfx";
    string postfix    = Nio::getPostfix();
    if(!postfix.empty())
        clientname += "_" + postfix;
    jack_status_t jackstatus;
    bool use_server_name = server.size() && server.compare("default") != 0;
    jack_options_t jopts = (jack_options_t)
                           (((!instance_name
                              && use_server_name) ? JackServerName :
                             JackNullOption)
                            | ((autostart_jack) ? JackNullOption :
                               JackNoStartServer));

    if(instance_name)
        jackClient = jack_client_open(instance_name, jopts, &jackstatus);
    else {
        if(use_server_name)
            jackClient = jack_client_open(
                clientname.c_str(), jopts, &jackstatus,
                server.c_str());
        else
            jackClient = jack_client_open(
                clientname.c_str(), jopts, &jackstatus);
    }


    if(NULL != jackClient)
        return true;
    else
        cerr << "Error, failed to open jack client on server: " << server
             << " status " << jackstatus << endl;
    return false;
}

bool JackEngine::connectJack()
{
    connectServer("");
    if(NULL != jackClient) {
        setBufferSize(jack_get_buffer_size(jackClient));
        int chk;
        jack_set_error_function(_errorCallback);
        jack_set_info_function(_infoCallback);
        if(jack_set_buffer_size_callback(jackClient, _bufferSizeCallback, this))
            cerr << "Error setting the bufferSize callback" << endl;
        if((chk = jack_set_xrun_callback(jackClient, _xrunCallback, this)))
            cerr << "Error setting jack xrun callback" << endl;
        if(jack_set_process_callback(jackClient, _processCallback, this)) {
            cerr << "Error, JackEngine failed to set process callback" << endl;
            return false;
        }
        if(jack_activate(jackClient)) {
            cerr << "Error, failed to activate jack client" << endl;
            return false;
        }

        return true;
    }
    else
        cerr << "Error, NULL jackClient through Start()" << endl;
    return false;
}

void JackEngine::disconnectJack()
{
    if(jackClient) {
        cout << "Deactivating and closing JACK client" << endl;

        jack_deactivate(jackClient);
        jack_client_close(jackClient);
        jackClient = NULL;
    }
}

bool JackEngine::Start()
{
    return openMidi() && openAudio();
}

void JackEngine::Stop()
{
    stopMidi();
    stopAudio();
}

void JackEngine::setMidiEn(bool nval)
{
    if(nval)
        openMidi();
    else
        stopMidi();
}

bool JackEngine::getMidiEn() const
{
    return midi.inport;
}

void JackEngine::setAudioEn(bool nval)
{
    if(nval)
        openAudio();
    else
        stopAudio();
}

bool JackEngine::getAudioEn() const
{
    return audio.ports[0];
}

bool JackEngine::openAudio()
{
    if(getAudioEn())
        return true;

    if(!getMidiEn())
        if(!connectJack())
            return false;


    const char *portnames[] = { "out_1", "out_2" };
    for(int port = 0; port < 2; ++port)
        audio.ports[port] = jack_port_register(
            jackClient,
            portnames[port],
            JACK_DEFAULT_AUDIO_TYPE,
            JackPortIsOutput
            | JackPortIsTerminal,
            0);
    if((NULL != audio.ports[0]) && (NULL != audio.ports[1])) {
        audio.jackSamplerate = jack_get_sample_rate(jackClient);
        audio.jackNframes    = jack_get_buffer_size(jackClient);
        samplerate = audio.jackSamplerate;
        bufferSize = audio.jackNframes;


        //Attempt to autoConnect when specified
        if(Nio::autoConnect) {
            const char **outPorts = jack_get_ports(
                jackClient,
                NULL,
                NULL,
                JackPortIsPhysical
                | JackPortIsInput);
            if(outPorts != NULL) {
                //Verify that stereo is available
                assert(outPorts[0]);
                assert(outPorts[1]);

                //Connect to physical outputs
                jack_connect(jackClient, jack_port_name(
                                 audio.ports[0]), outPorts[0]);
                jack_connect(jackClient, jack_port_name(
                                 audio.ports[1]), outPorts[1]);
            }
            else
                cerr << "Warning, No outputs to autoconnect to" << endl;
        }
        return true;
    }
    else
        cerr << "Error, failed to register jack audio ports" << endl;
    return false;
}

void JackEngine::stopAudio()
{
    for(int i = 0; i < 2; ++i) {
        jack_port_t *port = audio.ports[i];
        audio.ports[i] = NULL;
        if(NULL != port)
            jack_port_unregister(jackClient, port);
    }
    if(!getMidiEn())
        disconnectJack();
}

bool JackEngine::openMidi()
{
    if(getMidiEn())
        return true;
    if(!getAudioEn())
        if(!connectJack())
            return false;

    midi.inport = jack_port_register(jackClient, "midi_input",
                                     JACK_DEFAULT_MIDI_TYPE,
                                     JackPortIsInput | JackPortIsTerminal, 0);
    return midi.inport;
}

void JackEngine::stopMidi()
{
    jack_port_t *port = midi.inport;
    midi.inport = NULL;
    if(port)
        jack_port_unregister(jackClient, port);

    if(!getAudioEn())
        disconnectJack();
}

int JackEngine::clientId()
{
    if(NULL != jackClient)
        return (long)jack_client_thread_id(jackClient);
    else
        return -1;
}

string JackEngine::clientName()
{
    if(NULL != jackClient)
        return string(jack_get_client_name(jackClient));
    else
        cerr << "Error, clientName() with null jackClient" << endl;
    return string("Oh, yoshimi :-(");
}

int JackEngine::_processCallback(jack_nframes_t nframes, void *arg)
{
    return static_cast<JackEngine *>(arg)->processCallback(nframes);
}

int JackEngine::processCallback(jack_nframes_t nframes)
{
    bool okaudio = true;

    if((NULL != audio.ports[0]) && (NULL != audio.ports[1]))
        okaudio = processAudio(nframes);
    if(okaudio)
        handleMidi(nframes);
    return okaudio ? 0 : -1;
}

bool JackEngine::processAudio(jack_nframes_t nframes)
{
    for(int port = 0; port < 2; ++port) {
        audio.portBuffs[port] =
            (jsample_t *)jack_port_get_buffer(audio.ports[port], nframes);
        if(NULL == audio.portBuffs[port]) {
            cerr << "Error, failed to get jack audio port buffer: "
                 << port << endl;
            return false;
        }
    }

    Stereo<float *> smp = getNext();

    //Assumes size of smp.l == nframes
    memcpy(audio.portBuffs[0], smp.l, bufferSize * sizeof(float));
    memcpy(audio.portBuffs[1], smp.r, bufferSize * sizeof(float));
    return true;
}

int JackEngine::_xrunCallback(void *)
{
    cerr << "Jack reports xrun" << endl;
    return 0;
}

void JackEngine::_errorCallback(const char *msg)
{
    cerr << "Jack reports error: " << msg << endl;
}

void JackEngine::_infoCallback(const char *msg)
{
    cerr << "Jack info message: " << msg << endl;
}

int JackEngine::_bufferSizeCallback(jack_nframes_t nframes, void *arg)
{
    return static_cast<JackEngine *>(arg)->bufferSizeCallback(nframes);
}

int JackEngine::bufferSizeCallback(jack_nframes_t nframes)
{
    cerr << "Jack buffer resized" << endl;
    setBufferSize(nframes);
    return 0;
}

void JackEngine::handleMidi(unsigned long frames)
{
    if(!midi.inport)
        return;
    void *midi_buf = jack_port_get_buffer(midi.inport, frames);
    jack_midi_event_t jack_midi_event;
    jack_nframes_t    event_index = 0;
    unsigned char    *midi_data;
    unsigned char     type;

    while(jack_midi_event_get(&jack_midi_event, midi_buf,
                              event_index++) == 0) {
        MidiEvent ev;
        midi_data  = jack_midi_event.buffer;
        type       = midi_data[0] & 0xF0;
        ev.channel = midi_data[0] & 0x0F;

        switch(type) {
            case 0x80: /* note-off */
                ev.type  = M_NOTE;
                ev.num   = midi_data[1];
                ev.value = 0;
                InMgr::getInstance().putEvent(ev);
                break;

            case 0x90: /* note-on */
                ev.type  = M_NOTE;
                ev.num   = midi_data[1];
                ev.value = midi_data[2];
                InMgr::getInstance().putEvent(ev);
                break;

            case 0xA0: /* pressure, aftertouch */
                ev.type  = M_PRESSURE;
                ev.num   = midi_data[1];
                ev.value = midi_data[2];
                InMgr::getInstance().putEvent(ev);
                break;

            case 0xB0: /* controller */
                ev.type  = M_CONTROLLER;
                ev.num   = midi_data[1];
                ev.value = midi_data[2];
                InMgr::getInstance().putEvent(ev);
                break;

            case 0xC0: /* program change */
                ev.type  = M_PGMCHANGE;
                ev.num   = midi_data[1];
                ev.value = 0;
                InMgr::getInstance().putEvent(ev);
                break;

            case 0xE0: /* pitch bend */
                ev.type  = M_CONTROLLER;
                ev.num   = C_pitchwheel;
                ev.value = ((midi_data[2] << 7) | midi_data[1]) - 8192;
                InMgr::getInstance().putEvent(ev);
                break;

                /* XXX TODO: handle MSB/LSB controllers and RPNs and NRPNs */
        }
    }
}
