/*
    Rosegarden
    A MIDI and audio sequencer and musical notation editor.
 
    This program is Copyright 2000-2008
        Guillaume Laurent   <glaurent@telegraph-road.org>,
        Chris Cannam        <cannam@all-day-breakfast.com>,
        Richard Bown        <richard.bown@ferventsoftware.com>
 
    The moral rights of Guillaume Laurent, Chris Cannam, and Richard
    Bown to claim authorship of this work have been asserted.
 
    Other copyrights also apply to some parts of this work.  Please
    see the AUTHORS file and individual file headers for details.
 
    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.  See the file
    COPYING included with this distribution for more information.
*/

#ifdef HAVE_LIBLO

#include <lo/lo.h>
#include <unistd.h>

#include "AudioPluginOSCGUIManager.h"

#include "sound/Midi.h"
#include <tdelocale.h>
#include "misc/Debug.h"
#include "misc/Strings.h"
#include "AudioPluginOSCGUI.h"
#include "base/AudioPluginInstance.h"
#include "base/Exception.h"
#include "base/Instrument.h"
#include "base/MidiProgram.h"
#include "base/RealTime.h"
#include "base/Studio.h"
#include "gui/application/RosegardenGUIApp.h"
#include "OSCMessage.h"
#include "sound/MappedEvent.h"
#include "sound/PluginIdentifier.h"
#include "StudioControl.h"
#include "TimerCallbackAssistant.h"
#include <tqstring.h>


namespace Rosegarden
{

static void osc_error(int num, const char *msg, const char *path)
{
    std::cerr << "Rosegarden: ERROR: liblo server error " << num
	      << " in path " << path << ": " << msg << std::endl;
}

static int osc_message_handler(const char *path, const char *types, lo_arg **argv,
			       int argc, lo_message, void *user_data)
{
    AudioPluginOSCGUIManager *manager = (AudioPluginOSCGUIManager *)user_data;

    InstrumentId instrument;
    int position;
    TQString method;

    if (!manager->parseOSCPath(path, instrument, position, method)) {
	return 1;
    }

    OSCMessage *message = new OSCMessage();
    message->setTarget(instrument);
    message->setTargetData(position);
    message->setMethod(qstrtostr(method));

    int arg = 0;
    while (types && arg < argc && types[arg]) {
	message->addArg(types[arg], argv[arg]);
	++arg;
    }

    manager->postMessage(message);
    return 0;
}

AudioPluginOSCGUIManager::AudioPluginOSCGUIManager(RosegardenGUIApp *app) :
        m_app(app),
        m_studio(0),
        m_haveOSCThread(false),
        m_oscBuffer(1023),
        m_dispatchTimer(0)
{}

AudioPluginOSCGUIManager::~AudioPluginOSCGUIManager()
{
    delete m_dispatchTimer;

    for (TargetGUIMap::iterator i = m_guis.begin(); i != m_guis.end(); ++i) {
        for (IntGUIMap::iterator j = i->second.begin(); j != i->second.end();
             ++j) {
            delete j->second;
        }
    }
    m_guis.clear();

#ifdef HAVE_LIBLO_THREADSTOP

    if (m_haveOSCThread)
        lo_server_thread_stop(m_serverThread);
#endif
}

void
AudioPluginOSCGUIManager::checkOSCThread()
{
    if (m_haveOSCThread)
        return ;

    m_serverThread = lo_server_thread_new(NULL, osc_error);

    lo_server_thread_add_method(m_serverThread, NULL, NULL,
                                osc_message_handler, this);

    lo_server_thread_start(m_serverThread);

    RG_DEBUG << "AudioPluginOSCGUIManager: Base OSC URL is "
    << lo_server_thread_get_url(m_serverThread) << endl;

    m_dispatchTimer = new TimerCallbackAssistant(20, timerCallback, this);

    m_haveOSCThread = true;
}

bool
AudioPluginOSCGUIManager::hasGUI(InstrumentId instrument, int position)
{
    PluginContainer *container = 0;
    container = m_studio->getContainerById(instrument);
    if (!container) return false;

    AudioPluginInstance *pluginInstance = container->getPlugin(position);
    if (!pluginInstance) return false;

    try {
        TQString filePath = AudioPluginOSCGUI::getGUIFilePath
                           (strtoqstr(pluginInstance->getIdentifier()));
        return (!filePath.isNull() && filePath != "");
    } catch (Exception e) { // that's OK
        return false;
    }
}

void
AudioPluginOSCGUIManager::startGUI(InstrumentId instrument, int position)
{
    RG_DEBUG << "AudioPluginOSCGUIManager::startGUI: " << instrument << "," << position
    << endl;

    checkOSCThread();

    if (m_guis.find(instrument) != m_guis.end() &&
            m_guis[instrument].find(position) != m_guis[instrument].end()) {
        RG_DEBUG << "stopping GUI first" << endl;
        stopGUI(instrument, position);
    }

    // check the label
    PluginContainer *container = 0;
    container = m_studio->getContainerById(instrument);
    if (!container) {
        RG_DEBUG << "AudioPluginOSCGUIManager::startGUI: no such instrument or buss as "
                 << instrument << endl;
        return;
    }

    AudioPluginInstance *pluginInstance = container->getPlugin(position);
    if (!pluginInstance) {
        RG_DEBUG << "AudioPluginOSCGUIManager::startGUI: no plugin at position "
        << position << " for instrument " << instrument << endl;
        return ;
    }

    try {
        AudioPluginOSCGUI *gui =
            new AudioPluginOSCGUI(pluginInstance,
                                  getOSCUrl(instrument,
                                            position,
                                            strtoqstr(pluginInstance->getIdentifier())),
                                  getFriendlyName(instrument,
                                                  position,
                                                  strtoqstr(pluginInstance->getIdentifier())));
        m_guis[instrument][position] = gui;

    } catch (Exception e) {

        RG_DEBUG << "AudioPluginOSCGUIManager::startGUI: failed to start GUI: "
        << e.getMessage() << endl;
    }
}

void
AudioPluginOSCGUIManager::showGUI(InstrumentId instrument, int position)
{
    RG_DEBUG << "AudioPluginOSCGUIManager::showGUI: " << instrument << "," << position
    << endl;

    if (m_guis.find(instrument) != m_guis.end() &&
        m_guis[instrument].find(position) != m_guis[instrument].end()) {
        m_guis[instrument][position]->show();
    } else {
        startGUI(instrument, position);
    }
}

void
AudioPluginOSCGUIManager::stopGUI(InstrumentId instrument, int position)
{
    if (m_guis.find(instrument) != m_guis.end() &&
        m_guis[instrument].find(position) != m_guis[instrument].end()) {
        delete m_guis[instrument][position];
        m_guis[instrument].erase(position);
        if (m_guis[instrument].empty())
            m_guis.erase(instrument);
    }
}

void
AudioPluginOSCGUIManager::stopAllGUIs()
{
    while (!m_guis.empty()) {
        while (!m_guis.begin()->second.empty()) {
            delete (m_guis.begin()->second.begin()->second);
            m_guis.begin()->second.erase(m_guis.begin()->second.begin());
        }
        m_guis.erase(m_guis.begin());
    }
}

void
AudioPluginOSCGUIManager::postMessage(OSCMessage *message)
{
    RG_DEBUG << "AudioPluginOSCGUIManager::postMessage" << endl;
    m_oscBuffer.write(&message, 1);
}

void
AudioPluginOSCGUIManager::updateProgram(InstrumentId instrument, int position)
{
    RG_DEBUG << "AudioPluginOSCGUIManager::updateProgram(" << instrument << ","
    << position << ")" << endl;

    if (m_guis.find(instrument) == m_guis.end() ||
        m_guis[instrument].find(position) == m_guis[instrument].end())
        return ;

    PluginContainer *container = 0;
    container = m_studio->getContainerById(instrument);
    if (!container) return;

    AudioPluginInstance *pluginInstance = container->getPlugin(position);
    if (!pluginInstance) return;

    unsigned long rv = StudioControl::getPluginProgram
                       (pluginInstance->getMappedId(),
                        strtoqstr(pluginInstance->getProgram()));

    int bank = rv >> 16;
    int program = rv - (bank << 16);

    RG_DEBUG << "AudioPluginOSCGUIManager::updateProgram(" << instrument << ","
    << position << "): rv " << rv << ", bank " << bank << ", program " << program << endl;

    m_guis[instrument][position]->sendProgram(bank, program);
}

void
AudioPluginOSCGUIManager::updatePort(InstrumentId instrument, int position,
                                     int port)
{
    RG_DEBUG << "AudioPluginOSCGUIManager::updatePort(" << instrument << ","
    << position << "," << port << ")" << endl;

    if (m_guis.find(instrument) == m_guis.end() ||
        m_guis[instrument].find(position) == m_guis[instrument].end())
        return ;

    PluginContainer *container = 0;
    container = m_studio->getContainerById(instrument);
    if (!container) return;

    AudioPluginInstance *pluginInstance = container->getPlugin(position);
    if (!pluginInstance)
        return ;

    PluginPortInstance *porti = pluginInstance->getPort(port);
    if (!porti)
        return ;

    RG_DEBUG << "AudioPluginOSCGUIManager::updatePort(" << instrument << ","
    << position << "," << port << "): value " << porti->value << endl;

    m_guis[instrument][position]->sendPortValue(port, porti->value);
}

void
AudioPluginOSCGUIManager::updateConfiguration(InstrumentId instrument, int position,
        TQString key)
{
    RG_DEBUG << "AudioPluginOSCGUIManager::updateConfiguration(" << instrument << ","
    << position << "," << key << ")" << endl;

    if (m_guis.find(instrument) == m_guis.end() ||
            m_guis[instrument].find(position) == m_guis[instrument].end())
        return ;

    PluginContainer *container = m_studio->getContainerById(instrument);
    if (!container) return;

    AudioPluginInstance *pluginInstance = container->getPlugin(position);
    if (!pluginInstance) return;

    TQString value = strtoqstr(pluginInstance->getConfigurationValue(qstrtostr(key)));

    RG_DEBUG << "AudioPluginOSCGUIManager::updatePort(" << instrument << ","
    << position << "," << key << "): value " << value << endl;

    m_guis[instrument][position]->sendConfiguration(key, value);
}

TQString
AudioPluginOSCGUIManager::getOSCUrl(InstrumentId instrument, int position,
                                    TQString identifier)
{
    // OSC URL will be of the form
    //   osc.udp://localhost:54343/plugin/dssi/<instrument>/<position>/<label>
    // where <position> will be "synth" for synth plugins

    TQString type, soName, label;
    PluginIdentifier::parseIdentifier(identifier, type, soName, label);

    TQString baseUrl = lo_server_thread_get_url(m_serverThread);
    if (!baseUrl.endsWith("/"))
        baseUrl += '/';

    TQString url = TQString("%1%2/%3/%4/%5/%6")
                  .arg(baseUrl)
                  .arg("plugin")
                  .arg(type)
                  .arg(instrument);

    if (position == int(Instrument::SYNTH_PLUGIN_POSITION)) {
        url = url.arg("synth");
    } else {
        url = url.arg(position);
    }

    url = url.arg(label);

    return url;
}

bool
AudioPluginOSCGUIManager::parseOSCPath(TQString path, InstrumentId &instrument,
                                       int &position, TQString &method)
{
    RG_DEBUG << "AudioPluginOSCGUIManager::parseOSCPath(" << path << ")" << endl;
    if (!m_studio)
        return false;

    TQString pluginStr("/plugin/");

    if (path.startsWith("//")) {
        path = path.right(path.length() - 1);
    }

    if (!path.startsWith(pluginStr)) {
        RG_DEBUG << "AudioPluginOSCGUIManager::parseOSCPath: malformed path "
        << path << endl;
        return false;
    }

    path = path.right(path.length() - pluginStr.length());

    TQString type = path.section('/', 0, 0);
    TQString instrumentStr = path.section('/', 1, 1);
    TQString positionStr = path.section('/', 2, 2);
    TQString label = path.section('/', 3, -2);
    method = path.section('/', -1, -1);

    if (!instrumentStr || !positionStr) {
        RG_DEBUG << "AudioPluginOSCGUIManager::parseOSCPath: no instrument or position in " << path << endl;
        return false;
    }

    instrument = instrumentStr.toUInt();

    if (positionStr == "synth") {
        position = Instrument::SYNTH_PLUGIN_POSITION;
    } else {
        position = positionStr.toInt();
    }

    // check the label
    PluginContainer *container = m_studio->getContainerById(instrument);
    if (!container) {
        RG_DEBUG << "AudioPluginOSCGUIManager::parseOSCPath: no such instrument or buss as "
        << instrument << " in path " << path << endl;
        return false;
    }

    AudioPluginInstance *pluginInstance = container->getPlugin(position);
    if (!pluginInstance) {
        RG_DEBUG << "AudioPluginOSCGUIManager::parseOSCPath: no plugin at position "
        << position << " for instrument " << instrument << " in path "
        << path << endl;
        return false;
    }

    TQString identifier = strtoqstr(pluginInstance->getIdentifier());
    TQString iType, iSoName, iLabel;
    PluginIdentifier::parseIdentifier(identifier, iType, iSoName, iLabel);
    if (iLabel != label) {
        RG_DEBUG << "AudioPluginOSCGUIManager::parseOSCPath: wrong label for plugin"
        << " at position " << position << " for instrument " << instrument
        << " in path " << path << " (actual label is " << iLabel
        << ")" << endl;
        return false;
    }

    RG_DEBUG << "AudioPluginOSCGUIManager::parseOSCPath: good path " << path
    << ", got mapped id " << pluginInstance->getMappedId() << endl;

    return true;
}

TQString
AudioPluginOSCGUIManager::getFriendlyName(InstrumentId instrument, int position,
        TQString)
{
    PluginContainer *container = m_studio->getContainerById(instrument);
    if (!container)
        return i18n("Rosegarden Plugin");
    else {
        if (position == int(Instrument::SYNTH_PLUGIN_POSITION)) {
            return i18n("Rosegarden: %1").arg(strtoqstr(container->getPresentationName()));
        } else {
            return i18n("Rosegarden: %1: %2").arg(strtoqstr(container->getPresentationName()))
                   .arg(i18n("Plugin slot %1").arg(position));
        }
    }
}

void
AudioPluginOSCGUIManager::timerCallback(void *data)
{
    AudioPluginOSCGUIManager *manager = (AudioPluginOSCGUIManager *)data;
    manager->dispatch();
}

void
AudioPluginOSCGUIManager::dispatch()
{
    if (!m_studio)
        return ;

    while (m_oscBuffer.getReadSpace() > 0) {

        OSCMessage *message = 0;
        m_oscBuffer.read(&message, 1);

        int instrument = message->getTarget();
        int position = message->getTargetData();

        PluginContainer *container = m_studio->getContainerById(instrument);
        if (!container) continue;

        AudioPluginInstance *pluginInstance = container->getPlugin(position);
        if (!pluginInstance) continue;

        AudioPluginOSCGUI *gui = 0;

        if (m_guis.find(instrument) == m_guis.end()) {
            RG_DEBUG << "AudioPluginOSCGUIManager: no GUI for instrument "
            << instrument << endl;
        } else if (m_guis[instrument].find(position) == m_guis[instrument].end()) {
            RG_DEBUG << "AudioPluginOSCGUIManager: no GUI for instrument "
            << instrument << ", position " << position << endl;
        } else {
            gui = m_guis[instrument][position];
        }

        std::string method = message->getMethod();

        char type;
        const lo_arg *arg;

        // These generally call back on the RosegardenGUIApp.  We'd
        // like to emit signals, but making AudioPluginOSCGUIManager a
        // TQObject is problematic if it's only conditionally compiled.

        if (method == "control") {

            if (message->getArgCount() != 2) {
                RG_DEBUG << "AudioPluginOSCGUIManager: wrong number of args ("
                << message->getArgCount() << ") for control method"
                << endl;
                goto done;
            }
            if (!(arg = message->getArg(0, type)) || type != 'i') {
                RG_DEBUG << "AudioPluginOSCGUIManager: failed to get port number"
                << endl;
                goto done;
            }
            int port = arg->i;
            if (!(arg = message->getArg(1, type)) || type != 'f') {
                RG_DEBUG << "AudioPluginOSCGUIManager: failed to get port value"
                << endl;
                goto done;
            }
            float value = arg->f;

            RG_DEBUG << "AudioPluginOSCGUIManager: setting port " << port
            << " to value " << value << endl;

            m_app->slotChangePluginPort(instrument, position, port, value);

        } else if (method == "program") {

            if (message->getArgCount() != 2) {
                RG_DEBUG << "AudioPluginOSCGUIManager: wrong number of args ("
                << message->getArgCount() << ") for program method"
                << endl;
                goto done;
            }
            if (!(arg = message->getArg(0, type)) || type != 'i') {
                RG_DEBUG << "AudioPluginOSCGUIManager: failed to get bank number"
                << endl;
                goto done;
            }
            int bank = arg->i;
            if (!(arg = message->getArg(1, type)) || type != 'i') {
                RG_DEBUG << "AudioPluginOSCGUIManager: failed to get program number"
                << endl;
                goto done;
            }
            int program = arg->i;

            TQString programName = StudioControl::getPluginProgram
                                  (pluginInstance->getMappedId(), bank, program);

            m_app->slotChangePluginProgram(instrument, position, programName);

        } else if (method == "update") {

            if (message->getArgCount() != 1) {
                RG_DEBUG << "AudioPluginOSCGUIManager: wrong number of args ("
                << message->getArgCount() << ") for update method"
                << endl;
                goto done;
            }
            if (!(arg = message->getArg(0, type)) || type != 's') {
                RG_DEBUG << "AudioPluginOSCGUIManager: failed to get GUI URL"
                << endl;
                goto done;
            }
            TQString url = &arg->s;

            if (!gui) {
                RG_DEBUG << "AudioPluginOSCGUIManager: no GUI for update method"
                << endl;
                goto done;
            }

            gui->setGUIUrl(url);

            for (AudioPluginInstance::ConfigMap::const_iterator i =
                        pluginInstance->getConfiguration().begin();
                    i != pluginInstance->getConfiguration().end(); ++i) {

                TQString key = strtoqstr(i->first);
                TQString value = strtoqstr(i->second);

#ifdef DSSI_PROJECT_DIRECTORY_KEY

                if (key == PluginIdentifier::RESERVED_PROJECT_DIRECTORY_KEY) {
                    key = DSSI_PROJECT_DIRECTORY_KEY;
                }
#endif

                RG_DEBUG << "update: configuration: " << key << " -> "
                << value << endl;

                gui->sendConfiguration(key, value);
            }

            unsigned long rv = StudioControl::getPluginProgram
                               (pluginInstance->getMappedId(), strtoqstr(pluginInstance->getProgram()));

            int bank = rv >> 16;
            int program = rv - (bank << 16);
            gui->sendProgram(bank, program);

            int controlCount = 0;
            for (PortInstanceIterator i = pluginInstance->begin();
                    i != pluginInstance->end(); ++i) {
                gui->sendPortValue((*i)->number, (*i)->value);
                /* Avoid overloading the GUI if there are lots and lots of ports */
                if (++controlCount % 50 == 0)
                    usleep(300000);
            }

            gui->show();

        } else if (method == "configure") {

            if (message->getArgCount() != 2) {
                RG_DEBUG << "AudioPluginOSCGUIManager: wrong number of args ("
                << message->getArgCount() << ") for configure method"
                << endl;
                goto done;
            }

            if (!(arg = message->getArg(0, type)) || type != 's') {
                RG_DEBUG << "AudioPluginOSCGUIManager: failed to get configure key"
                << endl;
                goto done;
            }
            TQString key = &arg->s;

            if (!(arg = message->getArg(1, type)) || type != 's') {
                RG_DEBUG << "AudioPluginOSCGUIManager: failed to get configure value"
                << endl;
                goto done;
            }
            TQString value = &arg->s;

#ifdef DSSI_RESERVED_CONFIGURE_PREFIX

            if (key.startsWith(DSSI_RESERVED_CONFIGURE_PREFIX) ||
                    key == PluginIdentifier::RESERVED_PROJECT_DIRECTORY_KEY) {
                RG_DEBUG << "AudioPluginOSCGUIManager: illegal reserved configure call from gui: " << key << " -> " << value << endl;
                goto done;
            }
#endif

            RG_DEBUG << "AudioPluginOSCGUIManager: configure(" << key << "," << value
            << ")" << endl;

            m_app->slotChangePluginConfiguration(instrument, position,
#ifdef DSSI_GLOBAL_CONFIGURE_PREFIX
                                                 key.startsWith(DSSI_GLOBAL_CONFIGURE_PREFIX),
#else
                                                 false,
#endif
                                                 key, value);

        } else if (method == "midi") {

            if (message->getArgCount() != 1) {
                RG_DEBUG << "AudioPluginOSCGUIManager: wrong number of args ("
                << message->getArgCount() << ") for midi method"
                << endl;
                goto done;
            }
            if (!(arg = message->getArg(0, type)) || type != 'm') {
                RG_DEBUG << "AudioPluginOSCGUIManager: failed to get MIDI event"
                << endl;
                goto done;
            }

            RG_DEBUG << "AudioPluginOSCGUIManager: handling MIDI message" << endl;

            // let's only handle note on and note off

            int eventCode = arg->m[1];
            int eventType = eventCode & MIDI_MESSAGE_TYPE_MASK;
            if (eventType == MIDI_NOTE_ON ||
                    eventType == MIDI_NOTE_OFF) {
                MappedEvent ev(instrument,
                               MappedEvent::MidiNote,
                               MidiByte(arg->m[2]),
                               MidiByte(arg->m[3]),
                               RealTime::zeroTime,
                               RealTime::zeroTime,
                               RealTime::zeroTime);
                if (eventType == MIDI_NOTE_OFF)
                    ev.setVelocity(0);
                StudioControl::sendMappedEvent(ev);
            }

        } else if (method == "exiting") {

            RG_DEBUG << "AudioPluginOSCGUIManager: GUI exiting" << endl;
            stopGUI(instrument, position);
            m_app->slotPluginGUIExited(instrument, position);

        } else {

            RG_DEBUG << "AudioPluginOSCGUIManager: unknown method " << method << endl;
        }

done:
        delete message;
    }
}

}

#endif
