// Copyright 2024 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0

// #![deny(missing_docs)]

pub mod device;
pub use device::Device;

pub mod node;
use intmap::IntMap;
pub use node::{MediaClass, Node, NodeProps};

mod profile;
pub use profile::{Profile, ProfileClass};

mod route;
pub use route::PortType;
pub use route::{Route, RouteProps};

mod spa_utils;
pub use spa_utils::Channel;

use libspa::{
    param::{ParamType, format::FormatProperties}, pod::{self, Pod, serialize::PodSerializer}, utils::SpaTypes
};
use pipewire::{
    device::{DeviceChangeMask, DeviceListener}, main_loop::MainLoopWeak, metadata::MetadataListener, node::NodeListener, proxy::{ProxyListener, ProxyT}, types::ObjectType
};
use std::{cell::RefCell, rc::Rc};

pub type NodeId = u32;
pub type RouteId = u32;
pub type DeviceId = u32;
pub type ProfileId = i32;
pub type PipewireId = u32;

pub fn run(on_event: impl FnMut(Event) + Send + 'static) -> Sender {
    let (request_tx, request_rx) = pipewire::channel::channel();

    std::thread::spawn(move || {
        let on_event = Box::new(on_event);
        if let Err(why) = run_service(request_rx, on_event) {
            tracing::error!(?why, "failed to run pipewire thread");
        }
    });

    Sender(request_tx)
}

/// Monitor pipewire activity and
fn run_service(
    rx: pipewire::channel::Receiver<Request>,
    on_event: Box<dyn FnMut(Event)>,
) -> Result<(), pipewire::Error> {
    let main_loop = pipewire::main_loop::MainLoopRc::new(None)?;
    let context = pipewire::context::ContextRc::new(&main_loop, None)?;
    let core = context.connect_rc(None)?;
    let registry = core.get_registry_rc()?;

    let state = Rc::new(RefCell::new(State {
        main_loop: main_loop.downgrade(),
        proxies: Proxies {
            devices: IntMap::new(),
            metadata: IntMap::new(),
            nodes: IntMap::new(),
        },
        default_sink_name: String::new(),
        default_source_name: String::new(),
        nodes: IntMap::new(),
        active_routes: IntMap::new(),
        routes: IntMap::new(),
        node_devices: IntMap::new(),
        node_card_profile_device: IntMap::new(),
        node_props: IntMap::new(),
        on_event,
    }));

    let _request_handler = rx.attach(main_loop.loop_(), {
        let state = Rc::downgrade(&state);
        move |request| match request {
            Request::EnumerateDevice(id) => {
                if let Some(state) = state.upgrade() {
                    state.borrow_mut().enumerate_device(id);
                }
            }

            Request::SetRoute(id, card_profile_device, route, save) => {
                if let Some(state) = state.upgrade() {
                    state.borrow_mut().set_route(
                        id,
                        card_profile_device as i32,
                        route as i32,
                        save,
                    );
                }
            }

            Request::SetNodeVolume(id, volume, balance) => {
                tracing::debug!(id, volume, ?balance, "SetNodeVolume");
                if let Some(state) = state.upgrade() {
                    state.borrow_mut().set_node_volume(id, volume, balance);
                }
            }

            Request::SetNodeMute(id, mute) => {
                if let Some(state) = state.upgrade() {
                    state.borrow_mut().set_mute_node(id, mute);
                }
            }

            Request::SetProfile(id, index, save) => {
                if let Some(state) = state.upgrade() {
                    state.borrow_mut().set_profile(id, index, save);
                }
            }

            Request::SetMetadataProperty {
                name,
                subject,
                key,
                type_,
                value,
            } => {
                if let Some(state) = state.upgrade() {
                    let mut state = state.borrow_mut();
                    if let Some(object) = state.proxies.metadata.iter_mut().find_map(
                        |(_, (meta_name, object, ..))| {
                            if *meta_name == name {
                                Some(object)
                            } else {
                                None
                            }
                        },
                    ) {
                        object.set_property(subject, &key, type_.as_deref(), value.as_deref());
                    }
                }
            }

            Request::Quit => {
                if let Some(state) = state.upgrade() {
                    state.borrow_mut().quit();
                }
            }
        }
    });

    let registry_weak = registry.downgrade();

    let _registry_listener = registry
        .add_listener_local()
        .global(move |obj| {
            let Some(registry) = registry_weak.upgrade() else {
                return;
            };

            match obj.type_ {
                ObjectType::Device => {
                    let Ok(device) = registry.bind::<pipewire::device::Device, _>(obj) else {
                        return;
                    };

                    device.subscribe_params(&[
                        ParamType::EnumProfile,
                        ParamType::Profile,
                        ParamType::EnumRoute,
                        ParamType::Route,
                    ]);

                    let pw_id = device.upcast_ref().id();

                    let listener = device
                        .add_listener_local()
                        .info({
                            let state = Rc::downgrade(&state);
                            move |info| {
                                let change_mask = info.change_mask();
                                if change_mask == DeviceChangeMask::PARAMS {
                                    if let Some(state) = state.upgrade() {
                                        let state = state.borrow();
                                        let Some((_device_id, device, ..)) =
                                            state.proxies.devices.get(pw_id)
                                        else {
                                            return;
                                        };

                                        device.enum_params(
                                            1,
                                            Some(ParamType::EnumRoute),
                                            0,
                                            u32::MAX,
                                        );
                                        device.enum_params(1, Some(ParamType::Route), 0, u32::MAX);
                                        device.enum_params(
                                            1,
                                            Some(ParamType::EnumProfile),
                                            0,
                                            u32::MAX,
                                        );
                                        device.enum_params(
                                            1,
                                            Some(ParamType::Profile),
                                            0,
                                            u32::MAX,
                                        );
                                    }

                                    return;
                                }

                                if let Some(device) = Device::from_device(info) {
                                    if let Some(state) = state.upgrade() {
                                        state.borrow_mut().add_device(pw_id, device);
                                    }
                                }
                            }
                        })
                        .param({
                            let state = Rc::downgrade(&state);
                            move |_seq, param_type, index, _next, param| {
                                let Some(pod) = param else {
                                    return;
                                };

                                let Some(state) = state.upgrade() else {
                                    return;
                                };

                                let Some(&(device_id, ..)) =
                                    state.borrow().proxies.devices.get(pw_id)
                                else {
                                    return;
                                };

                                match param_type {
                                    ParamType::EnumProfile => {
                                        if let Some(profile) = Profile::from_pod(pod) {
                                            state
                                                .borrow_mut()
                                                .add_profile(device_id, index, profile);
                                        }
                                    }

                                    ParamType::EnumRoute => {
                                        if let Some(route) = Route::from_pod(pod) {
                                            state.borrow_mut().add_route(device_id, index, route);
                                        }
                                    }

                                    ParamType::Profile => {
                                        if let Some(profile) = Profile::from_pod(pod) {
                                            state.borrow_mut().active_profile(device_id, profile);
                                        }
                                    }

                                    ParamType::Route => {
                                        if let Some(route) = Route::from_pod(pod) {
                                            state
                                                .borrow_mut()
                                                .active_route(device_id, index, route);
                                        }
                                    }

                                    _ => (),
                                }
                            }
                        })
                        .register();

                    let proxy = device.upcast_ref();

                    let remove_listener = proxy
                        .add_listener_local()
                        .removed({
                            let state = Rc::downgrade(&state);
                            move || {
                                if let Some(state) = state.upgrade() {
                                    state.borrow_mut().remove_device(pw_id);
                                }
                            }
                        })
                        .register();

                    state
                        .borrow_mut()
                        .proxies
                        .devices
                        .insert(pw_id, (0, device, listener, remove_listener));
                }

                ObjectType::Node => {
                    let Ok(node) = registry.bind::<pipewire::node::Node, _>(obj) else {
                        return;
                    };

                    node.subscribe_params(&[ParamType::Props]);

                    let id = node.upcast_ref().id();

                    let listener = node
                        .add_listener_local()
                        .info({
                            let state = Rc::downgrade(&state);
                            move |info| {
                                if let Some(node) = Node::from_node(info)
                                    && let Some(state) = state.upgrade()
                                {
                                    state.borrow_mut().add_node(id, node);
                                }
                            }
                        })
                        .param({
                            let state = Rc::downgrade(&state);
                            move |_seq, param_type, _index, _next, param| {
                                let Some(pod) = param else {
                                    return;
                                };

                                let Some(state) = state.upgrade() else {
                                    return;
                                };

                                let Some(&(node_id, ..)) = state.borrow().proxies.nodes.get(id)
                                else {
                                    return;
                                };

                                match param_type {
                                    ParamType::Props => {
                                        if let Some(props) = NodeProps::from_pod(pod) {
                                            state.borrow_mut().set_node_props(node_id, props);
                                        }
                                    }

                                    _ => (),
                                }
                            }
                        })
                        .register();

                    let remove_listener = node
                        .upcast_ref()
                        .add_listener_local()
                        .removed({
                            let state = Rc::downgrade(&state);
                            move || {
                                if let Some(state) = state.upgrade() {
                                    state.borrow_mut().remove_node(id);
                                }
                            }
                        })
                        .register();

                    state
                        .borrow_mut()
                        .proxies
                        .nodes
                        .insert(id, (0, node, listener, remove_listener));
                }

                ObjectType::Metadata => {
                    let Some(props) = obj.props else {
                        return;
                    };

                    let Some(name) = props.get("metadata.name").map(String::from) else {
                        return;
                    };

                    let Ok(metadata) = registry.bind::<pipewire::metadata::Metadata, _>(obj) else {
                        return;
                    };

                    let id = metadata.upcast_ref().id();

                    let listener = metadata.add_listener_local();
                    let listener = match name.as_str() {
                        "default" => listener
                            .property({
                                let state = Rc::downgrade(&state);
                                move |_subject, key, _type, value| {
                                    let Some((key, value)) = key.zip(value) else {
                                        return 0;
                                    };

                                    match key {
                                        "default.audio.sink" => {
                                            tracing::info!(target:"audio-backend", value, "default.audio.sink");
                                            if let Ok(value) =
                                                serde_json::de::from_str::<DefaultAudio>(value)
                                                && let Some(state) = state.upgrade()
                                            {
                                                state
                                                    .borrow_mut()
                                                    .default_sink(value.name.to_owned())
                                            }
                                        }

                                        "default.audio.source" => {
                                            tracing::info!(target:"audio-backend", value, "default.audio.source");
                                            if let Ok(value) =
                                                serde_json::de::from_str::<DefaultAudio>(value)
                                                && let Some(state) = state.upgrade()
                                            {
                                                state
                                                    .borrow_mut()
                                                    .default_source(value.name.to_owned())
                                            }
                                        }

                                        _ => (),
                                    }

                                    0
                                }
                            })
                            .register(),

                        "sm-settings" => listener
                            .property({
                                let state = Rc::downgrade(&state);
                                move |_subject, key, _type, value| {
                                    let Some((key, value)) = key.zip(value) else {
                                        return 0;
                                    };

                                    match key {
                                        "node.features.audio.mono" => {
                                            if let Ok(value) =
                                                serde_json::de::from_str::<BooleanProperty>(value)
                                            {
                                                if let Some(state) = state.upgrade() {
                                                    state.borrow_mut().mono_audio(value.value);
                                                }
                                            }
                                        }

                                        _ => (),
                                    }

                                    0
                                }
                            })
                            .register(),

                        _ => listener.register(),
                    };

                    let remove_listener = metadata
                        .upcast_ref()
                        .add_listener_local()
                        .removed({
                            let state = Rc::downgrade(&state);
                            move || {
                                if let Some(state) = state.upgrade() {
                                    state.borrow_mut().remove_metadata(id);
                                }
                            }
                        })
                        .register();

                    state
                        .borrow_mut()
                        .proxies
                        .metadata
                        .insert(id, (name, metadata, listener, remove_listener));
                }
                _ => {}
            };
        })
        .register();

    main_loop.run();
    Ok(())
}

/// Response from pipewire
#[derive(Clone, Debug)]
pub enum Event {
    /// Set the active profile for a device
    ActiveProfile(DeviceId, Profile),
    /// Set the active route for a device
    ActiveRoute(DeviceId, u32, Route),
    /// A new device was detected.
    AddDevice(Device),
    /// A new node was detected.
    AddNode(Node),
    /// A profile was enumerated
    AddProfile(DeviceId, u32, Profile),
    /// A route was enumerated
    AddRoute(DeviceId, u32, Route),
    /// The default sink was changed.
    DefaultSink(String),
    /// The default source was changed.
    DefaultSource(String),
    /// Mono audio node setting changed.
    MonoAudio(bool),
    /// Emitted when the properties of a node has changed.
    NodeProperties(NodeId, NodeProps),
    /// A device with the given device_id was removed.
    RemoveDevice(DeviceId),
    /// A node with the given object_id was removed.
    RemoveNode(NodeId),
}

#[derive(Clone, Debug)]
pub enum Request {
    /// Request a device's routes, profiles, active routes, and active profile.
    EnumerateDevice(DeviceId),
    /// Set property of a metadata object
    SetMetadataProperty {
        name: String,
        subject: u32,
        key: String,
        type_: Option<String>,
        value: Option<String>,
    },
    /// Mute a node ID
    SetNodeMute(NodeId, bool),
    /// Set a device profile by profile index.
    SetProfile(DeviceId, u32, bool),
    /// Set a new volume
    SetNodeVolume(NodeId, f32, Option<f32>),
    /// Change route of a device
    SetRoute(DeviceId, u32, u32, bool),
    /// Stop the main loop and exit the thread.
    Quit,
}

#[derive(Copy, Clone, Debug, Default, Hash, Eq, PartialEq)]
pub enum Availability {
    #[default]
    Unknown,
    No,
    Yes,
}

#[derive(Copy, Clone, Debug, Default, Hash, Eq, PartialEq)]
pub enum Direction {
    Input,
    #[default]
    Output,
}

#[derive(serde::Deserialize)]
pub struct DefaultAudio<'a> {
    name: &'a str,
}

#[derive(serde::Deserialize)]
pub struct BooleanProperty {
    value: bool,
}

struct Proxies {
    devices: IntMap<
        PipewireId,
        (
            DeviceId,
            pipewire::device::Device,
            DeviceListener,
            ProxyListener,
        ),
    >,
    nodes: IntMap<PipewireId, (NodeId, pipewire::node::Node, NodeListener, ProxyListener)>,
    metadata: IntMap<
        PipewireId,
        (
            String,
            pipewire::metadata::Metadata,
            MetadataListener,
            ProxyListener,
        ),
    >,
}

struct State {
    main_loop: MainLoopWeak,
    /// Stores pipewire objects that we are monitoring.
    pub(self) proxies: Proxies,
    /// Default source node name.
    default_source_name: String,
    /// Default sink node name.
    default_sink_name: String,
    /// Associates the pipewire ID of a node to its node and device IDs.
    nodes: IntMap<PipewireId, (NodeId, Option<DeviceId>)>,
    /// Routes which are currently in use by devices.
    active_routes: IntMap<DeviceId, Vec<Route>>,
    /// Routes which are supported by devices.
    routes: IntMap<DeviceId, Vec<Route>>,
    /// Associates node objects to their device objects.
    node_devices: IntMap<NodeId, DeviceId>,
    /// Additional properties of nodes for managing volume, mute, etc.
    node_props: IntMap<NodeId, NodeProps>,
    /// Associates a node with a card profile device for matching nodes to routes.
    node_card_profile_device: IntMap<NodeId, u32>,
    /// Handle events and exit the loop when `true` is returned.
    on_event: Box<dyn FnMut(Event)>,
}

impl State {
    fn active_profile(&mut self, id: DeviceId, profile: Profile) {
        self.on_event(Event::ActiveProfile(id, profile));
    }

    fn active_route(&mut self, id: DeviceId, index: u32, route: Route) {
        // Keep a record of routes attached to a device for setting properties.
        // This will overwrite routes on updates to
        let routes = self.active_routes.entry(id).or_default();
        if index == 0 {
            *routes = vec![route.clone()];
        } else {
            if routes.len() < index as usize + 1 {
                let additional = (index as usize + 1) - routes.capacity();
                routes.reserve_exact(additional);
                routes.extend(std::iter::repeat_n(Route::default(), additional));
            }
            routes[index as usize] = route.clone();
        }

        self.on_event(Event::ActiveRoute(id, index, route));
    }

    fn add_device(&mut self, id: PipewireId, device: Device) {
        tracing::debug!(target: "audio-backend", id, "add_device {}", device.id);
        // Map the device's pipewire ID to its device ID
        if let Some(entry) = self.proxies.devices.get_mut(id) {
            entry.0 = device.id;
        };

        let device_id = device.id;
        self.on_event(Event::AddDevice(device));

        // Request the device's profiles and properties now that we've registered it.
        self.enumerate_device(device_id);
    }

    fn add_node(&mut self, id: PipewireId, node: Node) {
        // Map the device's pipewire ID to its device ID
        if let Some(entry) = self.proxies.nodes.get_mut(id) {
            entry.0 = node.object_id;
            // Request properties for this node now that we've registered it.
            entry.1.enum_params(0, Some(ParamType::Props), 0, u32::MAX);
        };

        // Track the node's node ID and device ID by its pipewire ID.
        self.nodes.insert(id, (node.object_id, node.device_id));

        // And the associated route device that the node is derived from.
        if let Some(card_profile_device) = node.card_profile_device {
            self.node_card_profile_device
                .insert(node.object_id, card_profile_device);
        }

        // Track the node's device ID by its node ID.
        if let Some(device_id) = node.device_id {
            self.node_devices.insert(node.object_id, device_id);
        }

        self.on_event(Event::AddNode(node));
    }

    fn add_profile(&mut self, id: DeviceId, index: u32, profile: Profile) {
        self.on_event(Event::AddProfile(id, index, profile));
    }

    fn add_route(&mut self, id: DeviceId, index: u32, route: Route) {
        // Keep a record of routes attached to a device for setting properties.
        // This will overwrite routes on updates to
        let routes = self.routes.entry(id).or_default();
        if index == 0 {
            *routes = vec![route.clone()];
        } else {
            if routes.len() < index as usize + 1 {
                let additional = (index as usize + 1) - routes.capacity();
                routes.reserve_exact(additional);
                routes.extend(std::iter::repeat_n(Route::default(), additional));
            }
            routes[index as usize] = route.clone();
        }
        self.on_event(Event::AddRoute(id, index, route));
    }

    /// Request a device's profiles and routes.
    fn enumerate_device(&mut self, id: DeviceId) {
        let Some(device) = self.device(id) else {
            return;
        };

        device.enum_params(0, Some(ParamType::EnumRoute), 0, u32::MAX);
        device.enum_params(0, Some(ParamType::Route), 0, u32::MAX);
        device.enum_params(0, Some(ParamType::EnumProfile), 0, u32::MAX);
        device.enum_params(0, Some(ParamType::Profile), 0, u32::MAX);
    }

    fn default_sink(&mut self, name: String) {
        if self.default_sink_name != name {
            self.default_sink_name = name.clone();
        }

        self.on_event(Event::DefaultSink(name));
    }

    fn default_source(&mut self, name: String) {
        if self.default_source_name != name {
            self.default_source_name = name.clone();
        }

        self.on_event(Event::DefaultSource(name));
    }

    fn active_node_route(&self, device_id: DeviceId, route_device: i32) -> Option<&Route> {
        self.active_routes
            .get(device_id)?
            .iter()
            .find(|r| r.device == route_device || r.devices.contains(&route_device))
    }

    fn mono_audio(&mut self, enabled: bool) {
        self.on_event(Event::MonoAudio(enabled))
    }

    fn on_event(&mut self, event: Event) {
        (self.on_event)(event);
    }

    fn quit(&mut self) {
        if let Some(main_loop) = self.main_loop.upgrade() {
            main_loop.quit();
        }
    }

    fn remove_device(&mut self, id: PipewireId) {
        if let Some((device_id, ..)) = self.proxies.devices.remove(id) {
            tracing::debug!(target: "audio-backend", id, "remove_device {device_id}");
            self.routes.remove(device_id);
            self.active_routes.remove(device_id);
            self.on_event(Event::RemoveDevice(device_id));
        }
    }

    fn remove_metadata(&mut self, id: PipewireId) {
        self.proxies.metadata.remove(id);
    }

    fn remove_node(&mut self, id: PipewireId) {
        if let Some((node_id, _)) = self.nodes.remove(id) {
            self.node_card_profile_device.remove(node_id);
            self.node_devices.remove(node_id);
            self.node_props.remove(node_id);
            self.on_event(Event::RemoveNode(node_id));
        }

        self.proxies.nodes.remove(id);
    }

    fn set_mute(&self, id: DeviceId, route_device: i32, route: &Route, mute: bool) {
        let Some(device) = self.device(id) else {
            return;
        };

        let route_props = pod::object!(
            SpaTypes::ObjectParamProps,
            ParamType::Props,
            pod::property!(FormatProperties(libspa_sys::SPA_PROP_mute), Bool, mute),
        );

        let buffer = std::io::Cursor::new(Vec::new());
        let Ok(serialized) = PodSerializer::serialize(
            buffer,
            &pod::Value::Object(pod::object!(
                SpaTypes::ObjectParamRoute,
                ParamType::Route,
                pod::property!(
                    FormatProperties(libspa_sys::SPA_PARAM_ROUTE_index),
                    Int,
                    route.index
                ),
                pod::property!(
                    FormatProperties(libspa_sys::SPA_PARAM_ROUTE_device),
                    Int,
                    route_device
                ),
                pod::property!(
                    FormatProperties(libspa_sys::SPA_PARAM_ROUTE_props),
                    Object,
                    route_props
                ),
                pod::property!(
                    FormatProperties(libspa_sys::SPA_PARAM_ROUTE_save),
                    Bool,
                    true
                )
            )),
        )
        .map(|(cursor, _)| cursor.into_inner()) else {
            return;
        };

        if let Some(param) = Pod::from_bytes(&serialized) {
            device.set_param(ParamType::Route, 0, param);
        }
    }

    fn set_mute_node(&self, id: NodeId, mute: bool) {
        // Prefer to mute the device instead of the node.
        // Muting a node will not emit a notification.
        if let Some((&device_id, &card_profile_device)) = self
            .node_devices
            .get(id)
            .zip(self.node_card_profile_device.get(id))
        {
            let card_profile_device = card_profile_device as i32;
            if let Some(route) = self.active_node_route(device_id, card_profile_device) {
                self.set_mute(device_id, card_profile_device, route, mute);
                return;
            };
        }

        let Some(node) = self.node(id) else {
            return;
        };

        let buffer = std::io::Cursor::new(Vec::new());
        let Ok(serialized) = PodSerializer::serialize(
            buffer,
            &pod::Value::Object(pod::object!(
                SpaTypes::ObjectParamProps,
                ParamType::Props,
                pod::property!(FormatProperties(libspa_sys::SPA_PROP_mute), Bool, mute),
            )),
        )
        .map(|(cursor, _)| cursor.into_inner()) else {
            return;
        };

        if let Some(param) = Pod::from_bytes(&serialized) {
            node.set_param(ParamType::Props, 0, param);
        }
    }

    fn set_route(
        &self,
        device_id: DeviceId,
        card_profile_device: i32,
        route_index: i32,
        save: bool,
    ) {
        let Some(device) = self.device(device_id) else {
            return;
        };

        tracing::debug!(target: "audio-backend", "set_route device_id {device_id}, route_index {route_index}");

        let buffer = std::io::Cursor::new(Vec::new());
        let Ok(serialized) = PodSerializer::serialize(
            buffer,
            &pod::Value::Object(pod::object!(
                SpaTypes::ObjectParamRoute,
                ParamType::Route,
                pod::property!(
                    FormatProperties(libspa_sys::SPA_PARAM_ROUTE_index),
                    Int,
                    route_index
                ),
                pod::property!(
                    FormatProperties(libspa_sys::SPA_PARAM_ROUTE_device),
                    Int,
                    card_profile_device
                ),
                pod::property!(
                    FormatProperties(libspa_sys::SPA_PARAM_ROUTE_save),
                    Bool,
                    save
                )
            )),
        )
        .map(|(cursor, _)| cursor.into_inner()) else {
            return;
        };

        if let Some(param) = Pod::from_bytes(&serialized) {
            device.set_param(ParamType::Route, 0, param);
            device.enum_params(0, Some(ParamType::Route), 0, u32::MAX);
        }
    }

    fn set_node_props(&mut self, id: NodeId, props: NodeProps) {
        let prev_props = self.node_props.entry(id).or_default();
        if *prev_props == props {
            return;
        }

        *prev_props = props.clone();
        self.on_event(Event::NodeProperties(id, props.clone()));
    }

    fn set_node_volume(&self, id: NodeId, volume: f32, balance: Option<f32>) {
        let Some(props) = self.node_props.get(id) else {
            tracing::debug!(target: "audio-backend", id, volume, balance, "set_node_volume: no props found");
            return;
        };

        // Prefer to change the volume of the device instead of the node.
        if let Some((&device_id, &card_profile_device)) = self
            .node_devices
            .get(id)
            .zip(self.node_card_profile_device.get(id))
        {
            let card_profile_device = card_profile_device as i32;
            if let Some(route) = self.active_node_route(device_id, card_profile_device) {
                tracing::debug!(
                    target: "audio-backend",
                    device_id,
                    card_profile_device,
                    "set_node_volume: setting volume"
                );
                self.set_volume(
                    device_id,
                    props,
                    card_profile_device,
                    route,
                    volume,
                    balance,
                );
                return;
            };
        }

        let Some(node) = self.node(id) else {
            return;
        };

        let buffer = std::io::Cursor::new(Vec::new());
        let Ok(serialized) = PodSerializer::serialize(
            buffer,
            &pod::Value::Object(pod::object!(
                SpaTypes::ObjectParamProps,
                ParamType::Props,
                pod::property!(FormatProperties(libspa_sys::SPA_PROP_mute), Bool, false),
                pod::property!(
                    FormatProperties(libspa_sys::SPA_PROP_channelVolumes),
                    ValueArray,
                    pod::ValueArray::Float(volume::to_channel_volumes(
                        props.channel_map.as_deref().unwrap_or_default(),
                        volume,
                        balance,
                    ))
                )
            )),
        )
        .map(|(cursor, _)| cursor.into_inner()) else {
            return;
        };

        if let Some(param) = Pod::from_bytes(&serialized) {
            node.set_param(ParamType::Props, 0, param);
        }
    }

    fn set_profile(&mut self, id: DeviceId, index: u32, save: bool) {
        let Some(device) = self.device(id) else {
            return;
        };

        tracing::debug!(target: "audio-backend", device = id, index, save, "set_profile");
        let buffer = std::io::Cursor::new(Vec::new());
        let Ok(serialized) = PodSerializer::serialize(
            buffer,
            &pod::Value::Object(pod::object!(
                SpaTypes::ObjectParamProfile,
                ParamType::Profile,
                pod::property!(
                    FormatProperties(libspa_sys::SPA_PARAM_PROFILE_index),
                    Int,
                    index as i32
                ),
                pod::property!(
                    FormatProperties(libspa_sys::SPA_PARAM_PROFILE_save),
                    Bool,
                    save
                )
            )),
        )
        .map(|(cursor, _)| cursor.into_inner()) else {
            return;
        };

        if let Some(param) = Pod::from_bytes(&serialized) {
            device.set_param(ParamType::Profile, 0, param);
        }
    }

    fn set_volume(
        &self,
        id: DeviceId,
        props: &NodeProps,
        card_profile_device: i32,
        route: &Route,
        volume: f32,
        balance: Option<f32>,
    ) {
        let Some(device) = self.device(id) else {
            tracing::warn!(target: "audio-backend", id, "device not found for ID");
            return;
        };

        tracing::debug!(target: "audio-backend", "set_volume device_id {id}, route_index {}, card_profile_device {card_profile_device}, route_device {}, route_name: {}", route.index, route.device, route.name);

        let route_props = pod::object!(
            SpaTypes::ObjectParamProps,
            ParamType::Props,
            pod::property!(FormatProperties(libspa_sys::SPA_PROP_mute), Bool, false),
            pod::property!(
                FormatProperties(libspa_sys::SPA_PROP_channelVolumes),
                ValueArray,
                pod::ValueArray::Float(if matches!(route.direction, Direction::Output) {
                    volume::to_channel_volumes(
                        props.channel_map.as_deref().unwrap_or_default(),
                        volume,
                        balance,
                    )
                } else {
                    vec![volume * volume * volume]
                })
            )
        );

        let buffer = std::io::Cursor::new(Vec::new());
        let Ok(serialized) = PodSerializer::serialize(
            buffer,
            &pod::Value::Object(pod::object!(
                SpaTypes::ObjectParamRoute,
                ParamType::Route,
                pod::property!(
                    FormatProperties(libspa_sys::SPA_PARAM_ROUTE_index),
                    Int,
                    route.index
                ),
                pod::property!(
                    FormatProperties(libspa_sys::SPA_PARAM_ROUTE_device),
                    Int,
                    card_profile_device
                ),
                pod::property!(
                    FormatProperties(libspa_sys::SPA_PARAM_ROUTE_props),
                    Object,
                    route_props
                ),
                pod::property!(
                    FormatProperties(libspa_sys::SPA_PARAM_ROUTE_save),
                    Bool,
                    true
                )
            )),
        )
        .map(|(cursor, _)| cursor.into_inner()) else {
            tracing::error!(target: "audio-backend", id, "failed to serialize pod in set_volume");
            return;
        };

        if let Some(param) = Pod::from_bytes(&serialized) {
            tracing::debug!(
                target: "audio-backend",
                id,
                card_profile_device,
                volume,
                balance,
                "setting volume parameters for route"
            );
            device.set_param(ParamType::Route, 0, param);
        }
    }

    fn device(&self, id: DeviceId) -> Option<&pipewire::device::Device> {
        self.proxies
            .devices
            .values()
            .find(|(device_id, ..)| id == *device_id)
            .map(|(_, device, ..)| device)
    }

    fn node(&self, id: NodeId) -> Option<&pipewire::node::Node> {
        self.proxies
            .nodes
            .values()
            .find(|(node_id, ..)| id == *node_id)
            .map(|(_, node, ..)| node)
    }
}

pub struct Sender(pipewire::channel::Sender<Request>);

impl Sender {
    pub fn send(&self, request: Request) -> Result<(), Request> {
        self.0.send(request)
    }
}

impl Drop for Sender {
    fn drop(&mut self) {
        _ = self.0.send(Request::Quit);
    }
}

impl std::fmt::Debug for Sender {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str("Sender")
    }
}

pub mod volume {
    use crate::Channel;

    /// Get the configured volume and balance based on a provided channel volumes array.
    pub fn from_channel_volumes(channels: &[f32]) -> (f32, Option<f32>) {
        let left_volume = channels.first().cloned().unwrap_or_default();
        let right_volume = channels.last().cloned().unwrap_or_default();

        if (left_volume - right_volume).abs() < f32::EPSILON {
            return (left_volume.powf(1.0 / 3.0), None);
        }

        let (volume, balance) = if left_volume >= right_volume {
            (left_volume, right_volume / left_volume)
        } else {
            (right_volume, (2.0 - (left_volume / right_volume)))
        };

        (volume.powf(1.0 / 3.0), Some(balance))
    }

    /// Create a channel volumes array based on the provided volume, balance, and channel positions.
    pub fn to_channel_volumes(
        channel_map: &[Channel],
        volume: f32,
        balance: Option<f32>,
    ) -> Vec<f32> {
        let volume = volume * volume * volume;
        if let Some(balance) = balance {
            let (left_volume, right_volume) = if balance >= 1.0 {
                ((volume * (balance - 2.0).abs()), volume)
            } else {
                (volume, volume * balance)
            };

            let center_volume = (left_volume + right_volume) / 2.0;
            let mut channel_volumes = Vec::with_capacity(channel_map.len());

            // Use channel identifiers to apply volume balance
            for channel in channel_map {
                channel_volumes.push(match channel {
                    // Left channels
                    Channel::FL
                    | Channel::SL
                    | Channel::FLC
                    | Channel::RL
                    | Channel::TFL
                    | Channel::TFC
                    | Channel::TRL
                    | Channel::RLC
                    | Channel::FLW
                    | Channel::FLH
                    | Channel::TFLC
                    | Channel::TSL
                    | Channel::LLFE
                    | Channel::BLC => left_volume,
                    // Right channels
                    Channel::FR
                    | Channel::SR
                    | Channel::FRC
                    | Channel::RR
                    | Channel::TFR
                    | Channel::TRC
                    | Channel::TRR
                    | Channel::RRC
                    | Channel::FRW
                    | Channel::FRH
                    | Channel::TFRC
                    | Channel::TSR
                    | Channel::RLFE
                    | Channel::BRC => right_volume,
                    // Center/neutral channels
                    _ => center_volume,
                });
            }

            channel_volumes
        } else {
            vec![volume; channel_map.len()]
        }
    }

    #[cfg(test)]
    mod test {
        use crate::Channel;

        #[test]
        fn volume_balance_to_channel_volumes() {
            // Test conversions to and from a channel
            let channel_map = &[Channel::FL, Channel::FR];
            let inputs = vec![
                ((0.77, Some(0.32)), &[0.45653298, 0.14609055]),
                ((0.77, Some(0.57)), &[0.45653298, 0.2602238]),
                ((0.77, Some(0.68)), &[0.45653298, 0.31044245]),
                ((0.77, Some(0.74)), &[0.45653298, 0.33783442]),
                ((0.77, Some(1.00)), &[0.45653298, 0.45653298]),
                ((0.77, Some(1.32)), &[0.31044242, 0.45653298]),
                ((0.77, Some(1.57)), &[0.19630916, 0.45653298]),
                ((0.77, Some(1.68)), &[0.14609058, 0.45653298]),
                ((0.77, Some(1.74)), &[0.118698575, 0.45653298]),
            ];

            for ((volume, balance), channel_volumes) in inputs {
                let out = super::to_channel_volumes(channel_map, volume, balance);
                assert_eq!(&out, channel_volumes);
                let res = super::from_channel_volumes(&out);
                assert!((volume - res.0).abs() < 0.01, "{} != {}", volume, res.0);
                assert!(
                    balance.map_or_else(
                        || res.1 == Some(1.0),
                        |b| res.1.map_or_else(|| b == 1.0, |r| (b - r).abs() < 0.01)
                    ),
                    "{:?} != {:?}",
                    balance,
                    res.1
                );
            }
        }
    }
}
