#include "pulsemodel.h"
#include "pulsedevice.h"

#include <tqapplication.h>
#include <tqtimer.h>
#include <algorithm>
#include <cmath>

// Custom events posted from the PA thread to the main thread.
static const int PA_EVENT        = TQEvent::User + 1;
static const int PA_SERVER_EVENT = TQEvent::User + 3;
static const int PA_CARD_EVENT   = TQEvent::User + 4;
static const int PA_DIED_EVENT   = TQEvent::User + 5;

struct PAServerEvent : public TQCustomEvent {
    TQString defaultSinkName;
    TQString defaultSourceName;
    PAServerEvent( const TQString &sink, const TQString &source )
        : TQCustomEvent(PA_SERVER_EVENT), defaultSinkName(sink), defaultSourceName(source) {}
};

struct PAEvent : public TQCustomEvent {
    enum Kind { DeviceAdded, DeviceRemoved };
    Kind              kind;
    AudioDevice::Category cat;
    uint32_t          paIndex;
    TQString           name;
    int               volume;
    bool              muted;
    uint8_t           channels;
    int               pan;
    TQString           monitorName;
    TQString           iconName;
    TQString           paName;       // raw PA name (sinks/sources only, for default tracking)
    uint32_t          parentIndex;      // for Playback: parent sink; for Recording: parent source
    uint32_t          monitorSourceIdx; // for Output: PA index of the monitor source
    uint32_t          cardIdx;      // for Output/Input: owning card PA index
    TQString           activePortName;
    TQValueList<PulseDevicePort> devPorts;

    PAEvent( Kind k, AudioDevice::Category c, uint32_t idx,
             const TQString &n = TQString(), int vol = 0, bool m = false,
             uint8_t ch = 2, int p = 0, const TQString &mon = TQString(),
             const TQString &icon = TQString(), uint32_t parent = PA_INVALID_INDEX,
             const TQString &pname = TQString() )
        : TQCustomEvent(PA_EVENT), kind(k), cat(c), paIndex(idx),
          name(n), volume(vol), muted(m), channels(ch), pan(p),
          monitorName(mon), iconName(icon), paName(pname), parentIndex(parent),
          monitorSourceIdx(PA_INVALID_INDEX), cardIdx(PA_INVALID_INDEX) {}
};

struct PACardEvent : public TQCustomEvent {
    struct Profile { TQString name; TQString description; bool available; };
    struct Port {
        TQString name; TQString description;
        int available; int direction; uint32_t type; TQString availGroup;
    };
    bool     removed;
    uint32_t index;
    TQString  name;
    TQString  description;
    TQString  activeProfile;
    TQString  vendor;
    TQString  product;
    TQString  formFactor;
    TQString  busType;
    TQValueList<Profile> profiles;
    TQValueList<Port>    ports;

    explicit PACardEvent( uint32_t idx )
        : TQCustomEvent(PA_CARD_EVENT), removed(true), index(idx) {}
    PACardEvent( uint32_t idx, const TQString &n, const TQString &d, const TQString &ap )
        : TQCustomEvent(PA_CARD_EVENT), removed(false), index(idx),
          name(n), description(d), activeProfile(ap) {}
};

// ---- helpers ----------------------------------------------------------------

static bool isGenericMediaName( const TQString &s )
{
    TQString l = s.lower().stripWhiteSpace();
    return l == "audio stream" || l == "alsa playback" || l == "playback"
        || l == "audio output" || l == "output" || l.isEmpty();
}

static TQString paCleanAppName( const char *raw )
{
    if ( !raw || !*raw ) return TQString();
    TQString s = TQString::fromUtf8( raw );

    // Strip "ALSA plug-in [Foo]" wrapper → "Foo"
    int lb = s.find('['), rb = s.findRev(']');
    if ( lb != -1 && rb > lb )
        s = s.mid( lb + 1, rb - lb - 1 ).stripWhiteSpace();

    // capitalise first letter
    if ( !s.isEmpty() )
        s[0] = s[0].upper();

    return s;
}

static TQString paStreamName( pa_proplist *pl, const char *fallback )
{
    TQString appName  = paCleanAppName( pa_proplist_gets(pl, PA_PROP_APPLICATION_NAME) );
    const char *mn   = pa_proplist_gets(pl, PA_PROP_MEDIA_NAME);
    TQString mediaName = mn ? TQString::fromUtf8(mn).stripWhiteSpace() : TQString();

    if ( !appName.isEmpty() && !isGenericMediaName(mediaName) && !mediaName.isEmpty() )
        return appName + " - " + mediaName;
    if ( !appName.isEmpty() )
        return appName;
    if ( !isGenericMediaName(mediaName) && !mediaName.isEmpty() )
        return mediaName;
    return fallback ? TQString::fromUtf8(fallback) : TQString();
}

static TQString paAppIcon( pa_proplist *pl )
{
    const char *s;
    if ( (s = pa_proplist_gets(pl, PA_PROP_APPLICATION_ICON_NAME)) && *s )
        return TQString::fromUtf8(s);
    if ( (s = pa_proplist_gets(pl, "application.process.binary")) && *s ) {
        TQString bin = TQString::fromUtf8(s).lower();
        // Some TDE apps register binary as "fooapp" but icon as "foo"
        if ( bin.endsWith("app") )
            return bin.left( bin.length() - 3 );
        return bin;
    }
    if ( (s = pa_proplist_gets(pl, PA_PROP_APPLICATION_NAME)) && *s )
        return TQString::fromUtf8(s).lower();
    return TQString();
}

static int paVolumeToPercent( const pa_cvolume &cv )
{
    pa_volume_t mx = pa_cvolume_max( &cv );
    return (int)std::lround( (double)mx / PA_VOLUME_NORM * 100.0 );
}

// Derive pan -50..+50 from the first two channels of a cvolume.
static int cvolumeToPan( const pa_cvolume &cv )
{
    if ( cv.channels < 2 ) return 0;
    double l = cv.values[0];
    double r = cv.values[1];
    double mx = std::max( l, r );
    if ( mx < 1.0 ) return 0;
    return (int)( ( r - l ) / mx * 50.0 );
}

static void applyCardEvent( const PACardEvent *ev, PulseCardInfo &info )
{
    info.name          = ev->name;
    info.description   = ev->description;
    info.activeProfile = ev->activeProfile;
    info.vendor        = ev->vendor;
    info.product       = ev->product;
    info.formFactor    = ev->formFactor;
    info.busType       = ev->busType;
    info.profiles.clear();
    info.ports.clear();
    for ( TQValueList<PACardEvent::Profile>::ConstIterator it = ev->profiles.begin();
          it != ev->profiles.end(); ++it ) {
        PulseCardProfile p;
        p.name = (*it).name; p.description = (*it).description; p.available = (*it).available;
        info.profiles.append( p );
    }
    for ( TQValueList<PACardEvent::Port>::ConstIterator it = ev->ports.begin();
          it != ev->ports.end(); ++it ) {
        PulseCardPort port;
        port.name = (*it).name; port.description = (*it).description;
        port.available = (*it).available; port.direction = (*it).direction;
        port.type = (*it).type; port.availabilityGroup = (*it).availGroup;
        info.ports.append( port );
    }
}

// ---- ctor / dtor ------------------------------------------------------------

PulseModel::PulseModel( TQObject *parent )
    : TQObject(parent), m_mainloop(0), m_context(0), m_levelViewers(0)
{
    m_sinks.setAutoDelete( true );
    m_sources.setAutoDelete( true );
    m_sinkInputs.setAutoDelete( true );
    m_sourceOutputs.setAutoDelete( true );
}

PulseModel::~PulseModel()
{
    close();
}

// ---- connect / disconnect ---------------------------------------------------

bool PulseModel::open()
{
    m_mainloop = pa_threaded_mainloop_new();
    if ( !m_mainloop ) return false;

    pa_mainloop_api *api = pa_threaded_mainloop_get_api( m_mainloop );
    m_context = pa_context_new( api, "tmix" );
    if ( !m_context ) {
        pa_threaded_mainloop_free( m_mainloop );
        m_mainloop = 0;
        return false;
    }

    pa_context_set_state_callback( m_context, contextStateCb, this );
    pa_threaded_mainloop_lock( m_mainloop );
    pa_threaded_mainloop_start( m_mainloop );
    pa_context_connect( m_context, 0, PA_CONTEXT_NOFLAGS, 0 );
    pa_threaded_mainloop_unlock( m_mainloop );
    return true;
}

void PulseModel::close()
{
    // Stop the PA thread first. After this returns the thread is dead and
    // no callbacks can fire, so we can touch PA objects without locking.
    if ( m_mainloop ) {
        pa_threaded_mainloop_stop( m_mainloop );
    }

    // With the thread stopped, detach devices without locking.
    forEachDevice( [](PulseDevice *dev) { dev->detach( true ); } );

    if ( m_context ) {
        pa_context_set_state_callback( m_context, 0, 0 );
        pa_context_disconnect( m_context );
        pa_context_unref( m_context );
        m_context = 0;
    }
    if ( m_mainloop ) {
        pa_threaded_mainloop_free( m_mainloop );
        m_mainloop = 0;
    }
    m_sinks.clear();
    m_sources.clear();
    m_sinkInputs.clear();
    m_sourceOutputs.clear();
}

// ---- public query -----------------------------------------------------------

TQPtrList<AudioDevice> PulseModel::devices( AudioDevice::Category cat ) const
{
    TQPtrList<AudioDevice> result;
    const TQPtrList<PulseDevice> *src = 0;
    switch ( cat ) {
        case AudioDevice::Output:    src = &m_sinks;         break;
        case AudioDevice::Input:     src = &m_sources;       break;
        case AudioDevice::Playback:  src = &m_sinkInputs;    break;
        case AudioDevice::Recording: src = &m_sourceOutputs; break;
    }
    if ( src )
        for ( TQPtrListIterator<PulseDevice> it(*src); *it; ++it )
            result.append( *it );
    return result;
}

// ---- PA thread callbacks (called from PA thread — only post events) ---------

void PulseModel::contextStateCb( pa_context *c, void *userdata )
{
    PulseModel *self = static_cast<PulseModel *>(userdata);
    switch ( pa_context_get_state(c) ) {
        case PA_CONTEXT_READY:
            pa_context_set_subscribe_callback( c, subscribeCb, self );
            pa_context_subscribe( c,
                (pa_subscription_mask_t)(
                    PA_SUBSCRIPTION_MASK_SINK |
                    PA_SUBSCRIPTION_MASK_SOURCE |
                    PA_SUBSCRIPTION_MASK_SINK_INPUT |
                    PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT |
                    PA_SUBSCRIPTION_MASK_SERVER |
                    PA_SUBSCRIPTION_MASK_CARD ),
                0, 0 );
            pa_operation_unref( pa_context_get_server_info( c, serverInfoCb, self ) );
            self->enumerateAll();
            break;
        case PA_CONTEXT_FAILED:
        case PA_CONTEXT_TERMINATED:
            TQApplication::postEvent( self, new TQCustomEvent(PA_DIED_EVENT) );
            break;
        default:
            break;
    }
}

void PulseModel::enumerateAll()
{
    // Called from PA thread (inside context state callback at READY).
    pa_operation_unref( pa_context_get_sink_info_list( m_context, sinkInfoCb, this ) );
    pa_operation_unref( pa_context_get_source_info_list( m_context, sourceInfoCb, this ) );
    pa_operation_unref( pa_context_get_sink_input_info_list( m_context, sinkInputInfoCb, this ) );
    pa_operation_unref( pa_context_get_source_output_info_list( m_context, sourceOutputInfoCb, this ) );
    pa_operation_unref( pa_context_get_card_info_list( m_context, cardInfoCb, this ) );
}

void PulseModel::serverInfoCb( pa_context *, const pa_server_info *info, void *userdata )
{
    if ( !info ) return;
    PulseModel *self = static_cast<PulseModel *>(userdata);
    TQApplication::postEvent( self, new PAServerEvent(
        TQString::fromUtf8( info->default_sink_name   ? info->default_sink_name   : "" ),
        TQString::fromUtf8( info->default_source_name ? info->default_source_name : "" ) ) );
}

void PulseModel::sinkInfoCb( pa_context *, const pa_sink_info *info, int eol, void *userdata )
{
    if ( eol || !info ) return;
    PulseModel *self = static_cast<PulseModel *>(userdata);
    PAEvent *ev = new PAEvent(
        PAEvent::DeviceAdded, AudioDevice::Output, info->index,
        TQString::fromUtf8( info->description ),
        paVolumeToPercent( info->volume ), info->mute != 0,
        info->volume.channels, cvolumeToPan( info->volume ),
        TQString::fromUtf8( info->monitor_source_name ),
        TQString::fromUtf8( pa_proplist_gets( info->proplist, PA_PROP_DEVICE_ICON_NAME ) ? : "" ),
        PA_INVALID_INDEX,
        TQString::fromUtf8( info->name ) );
    ev->cardIdx          = info->card;
    ev->monitorSourceIdx = info->monitor_source;
    ev->activePortName = info->active_port
        ? TQString::fromUtf8( info->active_port->name ) : TQString();
    for ( uint32_t i = 0; i < info->n_ports; ++i ) {
        PulseDevicePort p;
        p.name        = TQString::fromUtf8( info->ports[i]->name );
        p.description = TQString::fromUtf8( info->ports[i]->description );
        ev->devPorts.append( p );
    }
    TQApplication::postEvent( self, ev );
}

void PulseModel::sourceInfoCb( pa_context *, const pa_source_info *info, int eol, void *userdata )
{
    if ( eol || !info ) return;
    // Skip monitor sources — they're PA internals, not real inputs.
    if ( info->monitor_of_sink != PA_INVALID_INDEX ) return;
    PulseModel *self = static_cast<PulseModel *>(userdata);
    PAEvent *ev = new PAEvent(
        PAEvent::DeviceAdded, AudioDevice::Input, info->index,
        TQString::fromUtf8( info->description ),
        paVolumeToPercent( info->volume ), info->mute != 0,
        info->volume.channels, cvolumeToPan( info->volume ),
        TQString::fromUtf8( info->name ),
        TQString::fromUtf8( pa_proplist_gets( info->proplist, PA_PROP_DEVICE_ICON_NAME ) ? : "" ),
        PA_INVALID_INDEX,
        TQString::fromUtf8( info->name ) );
    ev->cardIdx        = info->card;
    ev->activePortName = info->active_port
        ? TQString::fromUtf8( info->active_port->name ) : TQString();
    for ( uint32_t i = 0; i < info->n_ports; ++i ) {
        PulseDevicePort p;
        p.name        = TQString::fromUtf8( info->ports[i]->name );
        p.description = TQString::fromUtf8( info->ports[i]->description );
        ev->devPorts.append( p );
    }
    TQApplication::postEvent( self, ev );
}

void PulseModel::sinkInputInfoCb( pa_context *, const pa_sink_input_info *info, int eol, void *userdata )
{
    if ( eol || !info ) return;
    PulseModel *self = static_cast<PulseModel *>(userdata);
    TQApplication::postEvent( self, new PAEvent(
        PAEvent::DeviceAdded, AudioDevice::Playback, info->index,
        paStreamName( info->proplist, info->name ),
        paVolumeToPercent( info->volume ), info->mute != 0,
        info->volume.channels, cvolumeToPan( info->volume ),
        TQString(),   // monitor name filled in by customEvent from parent sink
        paAppIcon( info->proplist ),
        info->sink ) );  // parent sink index for peak monitoring
}

void PulseModel::sourceOutputInfoCb( pa_context *, const pa_source_output_info *info, int eol, void *userdata )
{
    if ( eol || !info ) return;
    // Filter out peak-monitor streams (ours and other apps like pavucontrol).
    if ( info->resample_method && strcmp(info->resample_method, "peaks") == 0 ) return;
    PulseModel *self = static_cast<PulseModel *>(userdata);
    TQApplication::postEvent( self, new PAEvent(
        PAEvent::DeviceAdded, AudioDevice::Recording, info->index,
        paStreamName( info->proplist, info->name ),
        paVolumeToPercent( info->volume ), info->mute != 0,
        info->volume.channels, cvolumeToPan( info->volume ),
        TQString(), paAppIcon( info->proplist ),
        info->source ) );  // parentIndex = source being recorded from
}

void PulseModel::subscribeCb( pa_context *c, pa_subscription_event_type_t t, uint32_t idx, void *userdata )
{
    PulseModel *self = static_cast<PulseModel *>(userdata);

    pa_subscription_event_type_t facility = (pa_subscription_event_type_t)( t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK );
    pa_subscription_event_type_t evtype   = (pa_subscription_event_type_t)( t & PA_SUBSCRIPTION_EVENT_TYPE_MASK );

    if ( facility == PA_SUBSCRIPTION_EVENT_SERVER ) {
        pa_operation_unref( pa_context_get_server_info( c, serverInfoCb, self ) );
        return;
    }

    if ( facility == PA_SUBSCRIPTION_EVENT_CARD ) {
        if ( evtype == PA_SUBSCRIPTION_EVENT_REMOVE ) {
            TQApplication::postEvent( self, new PACardEvent( idx ) );
        } else {
            pa_operation *op = pa_context_get_card_info_by_index( c, idx, cardInfoCb, self );
            if ( op ) pa_operation_unref( op );
        }
        return;
    }

    AudioDevice::Category cat;
    switch ( facility ) {
        case PA_SUBSCRIPTION_EVENT_SINK:          cat = AudioDevice::Output;    break;
        case PA_SUBSCRIPTION_EVENT_SOURCE:        cat = AudioDevice::Input;     break;
        case PA_SUBSCRIPTION_EVENT_SINK_INPUT:    cat = AudioDevice::Playback;  break;
        case PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT: cat = AudioDevice::Recording; break;
        default: return;
    }

    if ( evtype == PA_SUBSCRIPTION_EVENT_REMOVE ) {
        TQApplication::postEvent( self, new PAEvent( PAEvent::DeviceRemoved, cat, idx ) );
        return;
    }

    // NEW or CHANGE — re-query to get current state.
    pa_operation *op = 0;
    switch ( facility ) {
        case PA_SUBSCRIPTION_EVENT_SINK:
            op = pa_context_get_sink_info_by_index( c, idx, sinkInfoCb, self );
            break;
        case PA_SUBSCRIPTION_EVENT_SOURCE:
            op = pa_context_get_source_info_by_index( c, idx, sourceInfoCb, self );
            break;
        case PA_SUBSCRIPTION_EVENT_SINK_INPUT:
            op = pa_context_get_sink_input_info( c, idx, sinkInputInfoCb, self );
            break;
        case PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT:
            op = pa_context_get_source_output_info( c, idx, sourceOutputInfoCb, self );
            break;
        default: break;
    }
    if ( op ) pa_operation_unref( op );
}

void PulseModel::cardInfoCb( pa_context *, const pa_card_info *info, int eol, void *userdata )
{
    if ( eol || !info ) return;
    PulseModel *self = static_cast<PulseModel *>(userdata);

    const char *desc    = pa_proplist_gets( info->proplist, PA_PROP_DEVICE_DESCRIPTION );
    const char *vendor  = pa_proplist_gets( info->proplist, PA_PROP_DEVICE_VENDOR_NAME );
    const char *product = pa_proplist_gets( info->proplist, PA_PROP_DEVICE_PRODUCT_NAME );
    const char *ff      = pa_proplist_gets( info->proplist, PA_PROP_DEVICE_FORM_FACTOR );
    const char *bus     = pa_proplist_gets( info->proplist, "device.bus" );

    PACardEvent *ev = new PACardEvent(
        info->index,
        TQString::fromUtf8( info->name ),
        TQString::fromUtf8( desc ? desc : info->name ),
        info->active_profile2 ? TQString::fromUtf8( info->active_profile2->name ) : TQString() );

    ev->vendor     = TQString::fromUtf8( vendor  ? vendor  : "" );
    ev->product    = TQString::fromUtf8( product ? product : "" );
    ev->formFactor = TQString::fromUtf8( ff      ? ff      : "" );
    ev->busType    = TQString::fromUtf8( bus     ? bus     : "" );

    for ( uint32_t i = 0; info->profiles2 && info->profiles2[i]; ++i ) {
        PACardEvent::Profile p;
        p.name        = TQString::fromUtf8( info->profiles2[i]->name );
        p.description = TQString::fromUtf8( info->profiles2[i]->description );
        p.available   = info->profiles2[i]->available != 0;
        ev->profiles.append( p );
    }

    for ( uint32_t i = 0; i < info->n_ports; ++i ) {
        PACardEvent::Port port;
        port.name       = TQString::fromUtf8( info->ports[i]->name );
        port.description= TQString::fromUtf8( info->ports[i]->description );
        port.available  = info->ports[i]->available;
        port.direction  = info->ports[i]->direction;
        port.type       = info->ports[i]->type;
        if ( info->ports[i]->availability_group )
            port.availGroup = TQString::fromUtf8( info->ports[i]->availability_group );
        ev->ports.append( port );
    }

    TQApplication::postEvent( self, ev );
}

// ---- main thread event handler ----------------------------------------------

void PulseModel::reconnect()
{
    open();
}

void PulseModel::customEvent( TQCustomEvent *e )
{
    if ( e->type() == PA_DIED_EVENT ) {
        if ( !m_mainloop ) return; // already handling a previous die event
        // Notify UI that all devices are gone, then tear down and retry.
        forEachDevice( [this](PulseDevice *dev) { emit deviceRemoved( dev ); } );
        // Zero out default-device pointers in connected UI before devices are deleted.
        emit defaultOutputChanged( 0 );
        emit defaultInputChanged( 0 );
        // Sever all remaining signal connections on every device so that
        // close() can delete them safely regardless of cross-device wiring.
        forEachDevice( [this](PulseDevice *dev) { disconnect( dev, 0, 0, 0 ); } );
        m_sinksByName.clear();
        m_sourcesByName.clear();
        m_sourceOutputToSource.clear();
        m_monitorSourceToSink.clear();
        m_cards.clear();
        close();
        TQTimer::singleShot( 2000, this, TQ_SLOT(reconnect()) );
        return;
    }
    if ( e->type() == PA_CARD_EVENT ) {
        PACardEvent *ev = static_cast<PACardEvent *>(e);
        if ( ev->removed ) {
            for ( TQValueList<PulseCardInfo>::Iterator it = m_cards.begin();
                  it != m_cards.end(); ++it ) {
                if ( it->index == ev->index ) {
                    m_cards.remove( it );
                    emit cardRemoved( ev->index );
                    return;
                }
            }
            return;
        }
        PulseCardInfo *existing = findCard( ev->index );
        if ( existing ) {
            applyCardEvent( ev, *existing );
            emit cardUpdated( ev->index );
        } else {
            PulseCardInfo info;
            info.index = ev->index;
            applyCardEvent( ev, info );
            m_cards.append( info );
            emit cardAdded( ev->index );
        }
        return;
    }

    if ( e->type() == PA_SERVER_EVENT ) {
        PAServerEvent *ev = static_cast<PAServerEvent *>(e);
        if ( ev->defaultSinkName != m_defaultSinkName ) {
            m_defaultSinkName = ev->defaultSinkName;
            emit defaultOutputChanged( defaultOutput() );
        }
        if ( ev->defaultSourceName != m_defaultSourceName ) {
            m_defaultSourceName = ev->defaultSourceName;
            emit defaultInputChanged( defaultInput() );
        }
        return;
    }
    if ( e->type() != PA_EVENT ) return;
    PAEvent *ev = static_cast<PAEvent *>(e);

    TQPtrList<PulseDevice> *list = 0;
    switch ( ev->cat ) {
        case AudioDevice::Output:    list = &m_sinks;         break;
        case AudioDevice::Input:     list = &m_sources;       break;
        case AudioDevice::Playback:  list = &m_sinkInputs;    break;
        case AudioDevice::Recording: list = &m_sourceOutputs; break;
    }

    if ( ev->kind == PAEvent::DeviceRemoved ) {
        PulseDevice *dev = findDevice( *list, ev->paIndex );
        if ( dev ) {
            // Remove from name maps if present.
            for ( TQMap<TQString,PulseDevice*>::Iterator it = m_sinksByName.begin(); it != m_sinksByName.end(); ++it ) {
                if ( it.data() == dev ) { m_sinksByName.remove(it); break; }
            }
            for ( TQMap<TQString,PulseDevice*>::Iterator it = m_sourcesByName.begin(); it != m_sourcesByName.end(); ++it ) {
                if ( it.data() == dev ) { m_sourcesByName.remove(it); break; }
            }
            // Clean up port tracking maps.
            if ( ev->cat == AudioDevice::Output ) {
                m_sinkCard.remove( ev->paIndex );
                m_sinkActivePort.remove( ev->paIndex );
                m_sinkPorts.remove( ev->paIndex );
                // Remove monitor→sink mapping for this sink.
                TQMap<uint32_t,uint32_t>::Iterator mi = m_monitorSourceToSink.begin();
                while ( mi != m_monitorSourceToSink.end() ) {
                    if ( mi.data() == ev->paIndex ) { m_monitorSourceToSink.remove(mi); break; }
                    ++mi;
                }
            } else if ( ev->cat == AudioDevice::Input ) {
                m_sourceCard.remove( ev->paIndex );
                m_sourceActivePort.remove( ev->paIndex );
                m_sourcePorts.remove( ev->paIndex );
            }
            // If a recording stream ended, decrement its parent source's count.
            if ( ev->cat == AudioDevice::Recording ) {
                TQMap<uint32_t,uint32_t>::Iterator si = m_sourceOutputToSource.find( ev->paIndex );
                if ( si != m_sourceOutputToSource.end() ) {
                    PulseDevice *src = findDevice( m_sources, si.data() );
                    if ( src ) src->adjustRecordingCount( -1 );
                    m_sourceOutputToSource.remove( si );
                }
            }
            emit deviceRemoved( dev );
            list->remove( dev );
        }
        return;
    }

    // For Playback streams, resolve the parent sink's monitor source name so the
    // level meter can attach a peak-detect stream to just this sink input.
    TQString monitorName = ev->monitorName;
    if ( ev->cat == AudioDevice::Playback && monitorName.isEmpty()
         && ev->parentIndex != PA_INVALID_INDEX ) {
        PulseDevice *sink = findDevice( m_sinks, ev->parentIndex );
        if ( sink ) monitorName = sink->monitorName();
    }

    // Store / update port data for sinks and sources.
    if ( ev->cat == AudioDevice::Output ) {
        m_sinkCard[ev->paIndex]   = ev->cardIdx;
        m_sinkPorts[ev->paIndex]  = ev->devPorts;
        if ( ev->monitorSourceIdx != PA_INVALID_INDEX )
            m_monitorSourceToSink[ev->monitorSourceIdx] = ev->paIndex;
        TQString oldPort = m_sinkActivePort.contains(ev->paIndex)
                          ? m_sinkActivePort[ev->paIndex] : TQString();
        m_sinkActivePort[ev->paIndex] = ev->activePortName;
        if ( ev->activePortName != oldPort )
            emit sinkUpdated( ev->paIndex );
    } else if ( ev->cat == AudioDevice::Input ) {
        m_sourceCard[ev->paIndex]   = ev->cardIdx;
        m_sourcePorts[ev->paIndex]  = ev->devPorts;
        TQString oldPort = m_sourceActivePort.contains(ev->paIndex)
                          ? m_sourceActivePort[ev->paIndex] : TQString();
        m_sourceActivePort[ev->paIndex] = ev->activePortName;
        if ( ev->activePortName != oldPort )
            emit sourceUpdated( ev->paIndex );
    }

    PulseDevice *dev = findDevice( *list, ev->paIndex );
    if ( dev ) {
        dev->update( ev->name, ev->volume, ev->muted, ev->channels, ev->pan, monitorName, ev->iconName );
        if ( ev->cat == AudioDevice::Playback && ev->parentIndex != PA_INVALID_INDEX )
            dev->setSinkIndex( ev->parentIndex );
    } else {
        dev = new PulseDevice( ev->cat, ev->paIndex, this );
        dev->setModel( this );
        dev->setPAContext( m_context, m_mainloop );
        dev->update( ev->name, ev->volume, ev->muted, ev->channels, ev->pan, monitorName, ev->iconName );
        if ( ev->cat == AudioDevice::Playback && ev->parentIndex != PA_INVALID_INDEX )
            dev->setSinkIndex( ev->parentIndex );
        if ( !ev->paName.isEmpty() ) {
            dev->setPaName( ev->paName );
        }
        list->append( dev );
        if ( !ev->paName.isEmpty() ) {
            if ( ev->cat == AudioDevice::Output )
                m_sinksByName.insert( ev->paName, dev );
            else if ( ev->cat == AudioDevice::Input )
                m_sourcesByName.insert( ev->paName, dev );
        }
        emit deviceAdded( dev );
        if ( ev->cat == AudioDevice::Output && ev->paName == m_defaultSinkName )
            emit defaultOutputChanged( dev );
        if ( ev->cat == AudioDevice::Input && ev->paName == m_defaultSourceName )
            emit defaultInputChanged( dev );
        // Forward Recording device level from its parent source (best-effort).
        if ( ev->cat == AudioDevice::Recording && ev->parentIndex != PA_INVALID_INDEX ) {
            m_sourceOutputToSource.insert( ev->paIndex, ev->parentIndex );
            PulseDevice *src = findDevice( m_sources, ev->parentIndex );
            if ( src ) {
                // Parent is a real hardware input (microphone).
                src->adjustRecordingCount( +1 );
                connect( src, TQ_SIGNAL(levelChanged(float)),
                         dev, TQ_SIGNAL(levelChanged(float)) );
            } else {
                // Parent may be a monitor source — forward the parent sink's level.
                TQMap<uint32_t,uint32_t>::ConstIterator si =
                    m_monitorSourceToSink.find( ev->parentIndex );
                if ( si != m_monitorSourceToSink.end() ) {
                    PulseDevice *sink = findDevice( m_sinks, si.data() );
                    if ( sink )
                        connect( sink, TQ_SIGNAL(levelChanged(float)),
                                 dev,  TQ_SIGNAL(levelChanged(float)) );
                }
            }
        }
    }
}

AudioDevice *PulseModel::defaultOutput() const
{
    if ( m_sinksByName.contains(m_defaultSinkName) )
        return m_sinksByName[m_defaultSinkName];
    return 0;
}

AudioDevice *PulseModel::defaultInput() const
{
    if ( m_sourcesByName.contains(m_defaultSourceName) )
        return m_sourcesByName[m_defaultSourceName];
    return 0;
}

void PulseModel::addLevelViewer()
{
    if ( ++m_levelViewers == 1 )
        forEachDevice( [](PulseDevice *dev) { dev->resumeMonitoring(); } );
}

void PulseModel::removeLevelViewer()
{
    if ( m_levelViewers > 0 && --m_levelViewers == 0 )
        forEachDevice( [](PulseDevice *dev) { dev->pauseMonitoring(); } );
}

void PulseModel::paOp( std::function<pa_operation*()> fn )
{
    pa_threaded_mainloop_lock( m_mainloop );
    pa_operation *op = fn();
    if ( op ) pa_operation_unref( op );
    pa_threaded_mainloop_unlock( m_mainloop );
}

void PulseModel::forEachDevice( std::function<void(PulseDevice*)> fn )
{
    TQPtrList<PulseDevice> *lists[] = { &m_sinks, &m_sources, &m_sinkInputs, &m_sourceOutputs };
    for ( int i = 0; i < 4; ++i )
        for ( TQPtrListIterator<PulseDevice> it(*lists[i]); *it; ++it )
            fn( *it );
}

PulseDevice *PulseModel::findDevice( TQPtrList<PulseDevice> &list, uint32_t paIndex )
{
    for ( TQPtrListIterator<PulseDevice> it(list); *it; ++it )
        if ( (*it)->paIndex() == paIndex )
            return *it;
    return 0;
}

// ---- operations called from UI (main thread) ---------------------------------

void PulseModel::setDefaultOutput( AudioDevice *dev )
{
    PulseDevice *pd = dynamic_cast<PulseDevice*>(dev);
    if ( !pd || pd->paName().isEmpty() || !m_context || !m_mainloop ) return;
    paOp( [&]{ return pa_context_set_default_sink( m_context, pd->paName().utf8().data(), 0, 0 ); } );
}

void PulseModel::setDefaultInput( AudioDevice *dev )
{
    PulseDevice *pd = dynamic_cast<PulseDevice*>(dev);
    if ( !pd || pd->paName().isEmpty() || !m_context || !m_mainloop ) return;
    paOp( [&]{ return pa_context_set_default_source( m_context, pd->paName().utf8().data(), 0, 0 ); } );
}

void PulseModel::moveSinkInputToSink( AudioDevice *playbackDev, AudioDevice *outputDev )
{
    PulseDevice *si   = dynamic_cast<PulseDevice*>(playbackDev);
    PulseDevice *sink = dynamic_cast<PulseDevice*>(outputDev);
    if ( !si || !sink || !m_context || !m_mainloop ) return;
    paOp( [&]{ return pa_context_move_sink_input_by_index( m_context, si->paIndex(), sink->paIndex(), 0, 0 ); } );
}

const PulseCardInfo *PulseModel::card( uint32_t index ) const
{
    for ( TQValueList<PulseCardInfo>::ConstIterator it = m_cards.begin();
          it != m_cards.end(); ++it )
        if ( (*it).index == index )
            return &(*it);
    return 0;
}

PulseCardInfo *PulseModel::findCard( uint32_t index )
{
    for ( TQValueList<PulseCardInfo>::Iterator it = m_cards.begin();
          it != m_cards.end(); ++it )
        if ( it->index == index )
            return &(*it);
    return 0;
}

void PulseModel::setCardProfile( uint32_t cardIndex, const TQString &profileName )
{
    if ( !m_context || !m_mainloop || profileName.isEmpty() ) return;
    paOp( [&]{ return pa_context_set_card_profile_by_index( m_context, cardIndex, profileName.utf8().data(), 0, 0 ); } );
}

TQValueList<PulseModel::DevicePortInfo> PulseModel::devicesForCard( uint32_t cardIndex,
    const TQPtrList<PulseDevice> &list,
    const TQMap<uint32_t,uint32_t> &cardMap,
    const TQMap<uint32_t,TQString> &activePortMap,
    const TQMap<uint32_t,TQValueList<PulseDevicePort>> &portsMap ) const
{
    TQValueList<DevicePortInfo> result;
    for ( TQPtrListIterator<PulseDevice> it(list); *it; ++it ) {
        uint32_t idx = (*it)->paIndex();
        if ( !cardMap.contains(idx) || cardMap[idx] != cardIndex )
            continue;
        DevicePortInfo info;
        info.paIndex     = idx;
        info.description = (*it)->name();
        info.activePort  = activePortMap.contains(idx) ? activePortMap[idx] : TQString();
        info.ports       = portsMap.contains(idx) ? portsMap[idx] : TQValueList<PulseDevicePort>();
        result.append( info );
    }
    return result;
}

TQValueList<PulseModel::DevicePortInfo> PulseModel::sinksForCard( uint32_t cardIndex ) const
{
    return devicesForCard( cardIndex, m_sinks, m_sinkCard, m_sinkActivePort, m_sinkPorts );
}

TQValueList<PulseModel::DevicePortInfo> PulseModel::sourcesForCard( uint32_t cardIndex ) const
{
    return devicesForCard( cardIndex, m_sources, m_sourceCard, m_sourceActivePort, m_sourcePorts );
}

void PulseModel::setSinkPort( uint32_t sinkPaIndex, const TQString &portName )
{
    if ( !m_context || !m_mainloop || portName.isEmpty() ) return;
    paOp( [&]{ return pa_context_set_sink_port_by_index( m_context, sinkPaIndex, portName.utf8().data(), 0, 0 ); } );
}

void PulseModel::setSourcePort( uint32_t sourcePaIndex, const TQString &portName )
{
    if ( !m_context || !m_mainloop || portName.isEmpty() ) return;
    paOp( [&]{ return pa_context_set_source_port_by_index( m_context, sourcePaIndex, portName.utf8().data(), 0, 0 ); } );
}

#include "pulsemodel.moc"
