root/src/kits/midi2/MidiRosterLooper.cpp
/*
 * Copyright 2006, Haiku.
 *
 * Copyright (c) 2002-2004 Matthijs Hollemans
 * Distributed under the terms of the MIT License.
 *
 * Authors:
 *              Matthijs Hollemans
 */

#include "debug.h"
#include <MidiConsumer.h>
#include <MidiProducer.h>
#include <MidiRoster.h>
#include "MidiRosterLooper.h"
#include "protocol.h"

using namespace BPrivate;


BMidiRosterLooper::BMidiRosterLooper()
        : BLooper("MidiRosterLooper")
{
        fInitLock = -1;
        fRoster = NULL;
        fWatcher = NULL;
}


BMidiRosterLooper::~BMidiRosterLooper()
{
        StopWatching();

        if (fInitLock >= B_OK) {
                delete_sem(fInitLock); 
        }

        // At this point, our list may still contain endpoints with a 
        // zero reference count. These objects are proxies for remote 
        // endpoints, so we can safely delete them. If the list also 
        // contains endpoints with a non-zero refcount (which can be 
        // either remote or local), we will output a warning message.
        // It would have been better to jump into the debugger, but I
        // did not want to risk breaking any (misbehaving) old apps.

        for (int32 t = 0; t < CountEndpoints(); ++t) {
                BMidiEndpoint* endp = EndpointAt(t);
                if (endp->fRefCount > 0) {
                        fprintf(
                                stderr, "[midi] WARNING: Endpoint %" B_PRId32 " (%p) has "
                                "not been Release()d properly (refcount = %" B_PRId32 ")\n",
                                endp->ID(), endp, endp->fRefCount);
                } else {
                        delete endp;
                }
        }
}


bool 
BMidiRosterLooper::Init(BMidiRoster* roster_)
{
        ASSERT(roster_ != NULL)

        fRoster = roster_;

        // We create a semaphore with a zero count. BMidiRoster's
        // MidiRoster() method will try to acquire this semaphore,
        // but blocks because the count is 0. When we receive the
        // "app registered" message in our MessageReceived() hook,
        // we release the semaphore and MidiRoster() will unblock.

        fInitLock = create_sem(0, "InitLock");

        if (fInitLock < B_OK) {
                WARN("Could not create semaphore")
                return false;
        }

        thread_id threadId = Run();

        if (threadId < B_OK) {
                WARN("Could not start looper thread") 
                return false;
        }

        return true;
}


BMidiEndpoint* 
BMidiRosterLooper::NextEndpoint(int32* id)
{
        ASSERT(id != NULL)

        for (int32 t = 0; t < CountEndpoints(); ++t) {
                BMidiEndpoint* endp = EndpointAt(t);
                if (endp->ID() > *id) {
                        if (endp->IsRemote() && endp->IsRegistered()) {
                                *id = endp->ID();
                                return endp;
                        }
                }
        }

        return NULL;
}


BMidiEndpoint* 
BMidiRosterLooper::FindEndpoint(int32 id)
{
        for (int32 t = 0; t < CountEndpoints(); ++t) {
                BMidiEndpoint* endp = EndpointAt(t);
                if (endp->ID() == id) {
                        return endp;
                }
        } 

        return NULL;
}


void 
BMidiRosterLooper::AddEndpoint(BMidiEndpoint* endp)
{
        ASSERT(endp != NULL)
        ASSERT(!fEndpoints.HasItem(endp))

        // We store the endpoints sorted by ID, because that
        // simplifies the implementation of NextEndpoint().
        // Although the midi_server assigns IDs in ascending
        // order, we can't assume that the mNEW messages also
        // are delivered in this order (mostly they will be).

        int32 t;
        for (t = CountEndpoints(); t > 0; --t) {
                BMidiEndpoint* other = EndpointAt(t - 1);
                if (endp->ID() > other->ID()) {
                        break;
                }       
        }
        fEndpoints.AddItem(endp, t);

        #ifdef DEBUG
        DumpEndpoints();
        #endif
}


void 
BMidiRosterLooper::RemoveEndpoint(BMidiEndpoint* endp)
{
        ASSERT(endp != NULL)
        ASSERT(fEndpoints.HasItem(endp))

        fEndpoints.RemoveItem(endp);

        if (endp->IsConsumer()) {
                DisconnectDeadConsumer((BMidiConsumer*) endp);
        } else {
                DisconnectDeadProducer((BMidiProducer*) endp);
        }
        
        #ifdef DEBUG
        DumpEndpoints();
        #endif
}


void 
BMidiRosterLooper::StartWatching(const BMessenger* watcher_)
{
        ASSERT(watcher_ != NULL)

        StopWatching();
        fWatcher = new BMessenger(*watcher_);

        AllEndpoints();
        AllConnections();
}


void 
BMidiRosterLooper::StopWatching()
{
        delete fWatcher;
        fWatcher = NULL;
}


void 
BMidiRosterLooper::MessageReceived(BMessage* msg)
{
        #ifdef DEBUG
        printf("IN "); msg->PrintToStream();
        #endif

        switch (msg->what) {
                case MSG_APP_REGISTERED:         OnAppRegistered(msg);         break;
                case MSG_ENDPOINT_CREATED:       OnEndpointCreated(msg);       break;
                case MSG_ENDPOINT_DELETED:       OnEndpointDeleted(msg);       break;
                case MSG_ENDPOINT_CHANGED:       OnEndpointChanged(msg);       break;
                case MSG_ENDPOINTS_CONNECTED:    OnConnectedDisconnected(msg); break;
                case MSG_ENDPOINTS_DISCONNECTED: OnConnectedDisconnected(msg); break;

                default: super::MessageReceived(msg); break;
        }       
}


void 
BMidiRosterLooper::OnAppRegistered(BMessage* msg)
{
        release_sem(fInitLock);
}


void 
BMidiRosterLooper::OnEndpointCreated(BMessage* msg)
{
        int32 id;
        bool isRegistered;
        BString name;
        BMessage properties;
        bool isConsumer;

        if ((msg->FindInt32("midi:id", &id) == B_OK)
                &&  (msg->FindBool("midi:registered", &isRegistered) == B_OK)
                &&  (msg->FindString("midi:name", &name) == B_OK)
                &&  (msg->FindMessage("midi:properties", &properties) == B_OK)
                &&  (msg->FindBool("midi:consumer", &isConsumer) == B_OK)) {
                if (isConsumer) {
                        int32 port;
                        bigtime_t latency;

                        if ((msg->FindInt32("midi:port", &port) == B_OK)
                                &&  (msg->FindInt64("midi:latency", &latency) == B_OK)) {
                                BMidiConsumer* cons = new BMidiConsumer();
                                cons->fName          = name;
                                cons->fId            = id;
                                cons->fIsRegistered  = isRegistered;
                                cons->fPort          = port;
                                cons->fLatency       = latency;
                                *(cons->fProperties) = properties;
                                AddEndpoint(cons);
                                return;
                        }
                } else { // producer
                        BMidiProducer* prod = new BMidiProducer();
                        prod->fName          = name;
                        prod->fId            = id;
                        prod->fIsRegistered  = isRegistered;
                        *(prod->fProperties) = properties;
                        AddEndpoint(prod);
                        return;
                }
        }

        WARN("Could not create proxy for remote endpoint")
}


void 
BMidiRosterLooper::OnEndpointDeleted(BMessage* msg)
{
        int32 id;
        if (msg->FindInt32("midi:id", &id) == B_OK) {
                BMidiEndpoint* endp = FindEndpoint(id);
                if (endp != NULL) {
                        RemoveEndpoint(endp);

                        // If the client is watching, and the endpoint is
                        // registered remote, we need to let it know that
                        // the endpoint is now unregistered.

                        if (endp->IsRemote() && endp->IsRegistered()) {
                                if (fWatcher != NULL) {
                                        BMessage notify;
                                        notify.AddInt32("be:op", B_MIDI_UNREGISTERED);
                                        ChangeEvent(&notify, endp);
                                }
                        }

                        // If the proxy object for this endpoint is no 
                        // longer being used, we can delete it. However, 
                        // if the refcount is not zero, we must defer 
                        // destruction until the client Release()'s the 
                        // object. We clear the "isRegistered" flag to
                        // let the client know the object is now invalid.

                        if (endp->fRefCount == 0) {
                                delete endp;
                        } else { // still being used
                                endp->fIsRegistered = false;
                                endp->fIsAlive = false;
                        }

                        return;
                }
        }

        WARN("Could not delete proxy for remote endpoint")
}


void 
BMidiRosterLooper::OnEndpointChanged(BMessage* msg)
{
        int32 id;
        if (msg->FindInt32("midi:id", &id) == B_OK) {
                BMidiEndpoint* endp = FindEndpoint(id);
                if ((endp != NULL) && endp->IsRemote()) {
                        ChangeRegistered(msg, endp);
                        ChangeName(msg, endp);
                        ChangeProperties(msg, endp);
                        ChangeLatency(msg, endp);

                        #ifdef DEBUG
                        DumpEndpoints();
                        #endif

                        return;
                }
        }

        WARN("Could not change endpoint attributes")
}


void 
BMidiRosterLooper::OnConnectedDisconnected(BMessage* msg)
{
        int32 prodId, consId;
        if ((msg->FindInt32("midi:producer", &prodId) == B_OK)
                &&  (msg->FindInt32("midi:consumer", &consId) == B_OK)) {
                BMidiEndpoint* endp1 = FindEndpoint(prodId);
                BMidiEndpoint* endp2 = FindEndpoint(consId);

                if ((endp1 != NULL) && endp1->IsProducer()) {
                        if ((endp2 != NULL) && endp2->IsConsumer()) {
                                BMidiProducer* prod = (BMidiProducer*) endp1;
                                BMidiConsumer* cons = (BMidiConsumer*) endp2;

                                bool mustConnect = (msg->what == MSG_ENDPOINTS_CONNECTED);

                                if (mustConnect) {
                                        prod->ConnectionMade(cons);
                                } else {
                                        prod->ConnectionBroken(cons);
                                }

                                if (fWatcher != NULL) {
                                        ConnectionEvent(prod, cons, mustConnect);
                                }

                                #ifdef DEBUG
                                DumpEndpoints();
                                #endif

                                return;
                        }
                }
        }

        WARN("Could not connect/disconnect endpoints")
}


void 
BMidiRosterLooper::ChangeRegistered(BMessage* msg, BMidiEndpoint* endp)
{
        ASSERT(msg != NULL)
        ASSERT(endp != NULL)

        bool isRegistered;
        if (msg->FindBool("midi:registered", &isRegistered) == B_OK) {
                if (endp->fIsRegistered != isRegistered) {
                        endp->fIsRegistered = isRegistered;

                        if (fWatcher != NULL) {
                                BMessage notify;
                                if (isRegistered) {
                                        notify.AddInt32("be:op", B_MIDI_REGISTERED);
                                } else {
                                        notify.AddInt32("be:op", B_MIDI_UNREGISTERED);
                                }
                                ChangeEvent(&notify, endp);
                        }
                }
        }
}


void 
BMidiRosterLooper::ChangeName(BMessage* msg, BMidiEndpoint* endp)
{
        ASSERT(msg != NULL)
        ASSERT(endp != NULL)

        BString name;
        if (msg->FindString("midi:name", &name) == B_OK) {
                if (endp->fName != name) {
                        endp->fName = name;

                        if ((fWatcher != NULL) && endp->IsRegistered()) {
                                BMessage notify;
                                notify.AddInt32("be:op", B_MIDI_CHANGED_NAME);
                                notify.AddString("be:name", name);
                                ChangeEvent(&notify, endp);
                        }
                }
        }
}


void 
BMidiRosterLooper::ChangeProperties(BMessage* msg, BMidiEndpoint* endp)
{
        ASSERT(msg != NULL)
        ASSERT(endp != NULL)

        BMessage properties;
        if (msg->FindMessage("midi:properties", &properties) == B_OK) {
                *(endp->fProperties) = properties;

                if ((fWatcher != NULL) && endp->IsRegistered()) {
                        BMessage notify;
                        notify.AddInt32("be:op", B_MIDI_CHANGED_PROPERTIES);
                        notify.AddMessage("be:properties", &properties);
                        ChangeEvent(&notify, endp);
                }
        }
}


void 
BMidiRosterLooper::ChangeLatency(BMessage* msg, BMidiEndpoint* endp)
{
        ASSERT(msg != NULL)
        ASSERT(endp != NULL)

        bigtime_t latency;
        if (msg->FindInt64("midi:latency", &latency) == B_OK) {
                if (endp->IsConsumer()) {
                        BMidiConsumer* cons = (BMidiConsumer*) endp;
                        if (cons->fLatency != latency) {
                                cons->fLatency = latency;

                                if ((fWatcher != NULL) && cons->IsRegistered()) {
                                        BMessage notify;
                                        notify.AddInt32("be:op", B_MIDI_CHANGED_LATENCY);
                                        notify.AddInt64("be:latency", latency);
                                        ChangeEvent(&notify, endp);
                                }
                        }
                }
        }
}


void 
BMidiRosterLooper::AllEndpoints()
{
        BMessage notify;
        for (int32 t = 0; t < CountEndpoints(); ++t) {
                BMidiEndpoint* endp = EndpointAt(t);
                if (endp->IsRemote() && endp->IsRegistered()) {
                        notify.MakeEmpty();
                        notify.AddInt32("be:op", B_MIDI_REGISTERED);
                        ChangeEvent(&notify, endp);
                }
        }
}


void 
BMidiRosterLooper::AllConnections()
{
        for (int32 t = 0; t < CountEndpoints(); ++t) {
                BMidiEndpoint* endp = EndpointAt(t);
                if (endp->IsRemote() && endp->IsRegistered()) {
                        if (endp->IsProducer()) {
                                BMidiProducer* prod = (BMidiProducer*) endp;
                                if (prod->LockProducer()) {
                                        for (int32 k = 0; k < prod->CountConsumers(); ++k) {
                                                ConnectionEvent(prod, prod->ConsumerAt(k), true);
                                        }
                                        prod->UnlockProducer();
                                }
                        }
                }
        }
}


void 
BMidiRosterLooper::ChangeEvent(BMessage* msg, BMidiEndpoint* endp)
{
        ASSERT(fWatcher != NULL)
        ASSERT(msg != NULL)
        ASSERT(endp != NULL)

        msg->what = B_MIDI_EVENT;
        msg->AddInt32("be:id", endp->ID());

        if (endp->IsConsumer()) {
                msg->AddString("be:type", "consumer");
        } else {
                msg->AddString("be:type", "producer");
        }

        fWatcher->SendMessage(msg);
}


void 
BMidiRosterLooper::ConnectionEvent(
        BMidiProducer* prod, BMidiConsumer* cons, bool mustConnect)
{
        ASSERT(fWatcher != NULL)
        ASSERT(prod != NULL)
        ASSERT(cons != NULL)

        BMessage notify;
        notify.what = B_MIDI_EVENT;
        notify.AddInt32("be:producer", prod->ID());
        notify.AddInt32("be:consumer", cons->ID());

        if (mustConnect) {
                notify.AddInt32("be:op", B_MIDI_CONNECTED);
        } else {
                notify.AddInt32("be:op", B_MIDI_DISCONNECTED);
        }

        fWatcher->SendMessage(&notify);
}


void 
BMidiRosterLooper::DisconnectDeadConsumer(BMidiConsumer* cons)
{
        ASSERT(cons != NULL)

        // Note: Rather than looping through each producer's list
        // of connected consumers, we let ConnectionBroken() tell
        // us whether the consumer really was connected.

        for (int32 t = 0; t < CountEndpoints(); ++t) {
                BMidiEndpoint* endp = EndpointAt(t);
                if (endp->IsProducer()) {
                        BMidiProducer* prod = (BMidiProducer*) endp;
                        if (prod->ConnectionBroken(cons)) {
                                if (cons->IsRemote() && (fWatcher != NULL)) {
                                        ConnectionEvent(prod, cons, false);
                                }
                        }
                }
        }
}


void 
BMidiRosterLooper::DisconnectDeadProducer(BMidiProducer* prod)
{
        ASSERT(prod != NULL)

        // We don't need to lock or remove the consumers from
        // the producer's list of connections, because when this
        // function is called, we're destroying the object.

        if (prod->IsRemote() && (fWatcher != NULL)) {
                for (int32 t = 0; t < prod->CountConsumers(); ++t) {
                        ConnectionEvent(prod, prod->ConsumerAt(t), false);
                }
        }
}


int32 
BMidiRosterLooper::CountEndpoints()
{
        return fEndpoints.CountItems();
}


BMidiEndpoint* 
BMidiRosterLooper::EndpointAt(int32 index)
{
        ASSERT(index >= 0 && index < CountEndpoints())

        return (BMidiEndpoint*) fEndpoints.ItemAt(index);
}


#ifdef DEBUG
void 
BMidiRosterLooper::DumpEndpoints()
{
        if (Lock()) {
                printf("*** START DumpEndpoints\n");

                for (int32 t = 0; t < CountEndpoints(); ++t) {
                        BMidiEndpoint* endp = EndpointAt(t);

                        printf("\tendpoint %" B_PRId32 " (%p):\n", t, endp);

                        printf(
                                "\t\tid %" B_PRId32 ", name '%s', %s, %s, %s, %s, refcount %"
                                B_PRId32 "\n", endp->ID(), endp->Name(),
                                endp->IsConsumer() ? "consumer" : "producer", 
                                endp->IsRegistered() ? "registered" : "unregistered", 
                                endp->IsLocal() ? "local" : "remote", 
                                endp->IsValid() ? "valid" : "invalid", endp->fRefCount);

                        printf("\t\tproperties: "); 
                        endp->fProperties->PrintToStream();

                        if (endp->IsConsumer()) {
                                BMidiConsumer* cons = (BMidiConsumer*) endp;
                                printf("\t\tport %" B_PRId32 ", latency %" B_PRIdBIGTIME "\n",
                                                cons->fPort, cons->fLatency);
                        } else {
                                BMidiProducer* prod = (BMidiProducer*) endp;
                                if (prod->LockProducer()) {
                                        printf("\t\tconnections:\n");
                                        for (int32 k = 0; k < prod->CountConsumers(); ++k) {
                                                BMidiConsumer* cons = prod->ConsumerAt(k);
                                                printf("\t\t\tid %" B_PRId32 " (%p)\n", cons->ID(),
                                                        cons);
                                        }
                                        prod->UnlockProducer();
                                }
                        }
                }

                printf("*** END DumpEndpoints\n");
                Unlock();
        }
}
#endif